Skip to content

Commit e1bd336

Browse files
Merge pull request #484 from iriusrisk/release/1.38.0
[release/1.38.0] to main
2 parents 536e80a + 7e51e5d commit e1bd336

61 files changed

Lines changed: 77162 additions & 105 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/qa.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ jobs:
1414
name: SonarCloud Analysis
1515
uses: ./.github/workflows/sonar.yml
1616
with:
17-
python-version: "3.11"
18-
secrets: inherit
17+
python-version: "3.12"
18+
secrets:
19+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
1920
test:
2021
name: StartLeft Tests
2122
strategy:

.github/workflows/sonar.yml

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -40,32 +40,30 @@ jobs:
4040
- name: Generate coverage report
4141
run: coverage xml
4242
- name: Analyze with SonarCloud
43-
# You can pin the exact commit or the version.
44-
# uses: SonarSource/sonarcloud-github-action@commithas or tag
45-
uses: SonarSource/sonarcloud-github-action@49e6cd3b187936a73b8280d59ffd9da69df63ec9 #v2.1.1
43+
uses: SonarSource/sonarqube-scan-action@1a6d90ebcb0e6a6b1d87e37ba693fe453195ae25 #v5.3.1
4644
env:
47-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information
48-
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # Generate a token on Sonarcloud.io, add it to the secrets of this repo with the name SONAR_TOKEN (Settings > Secrets > Actions > add new repository secret)
45+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information
46+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # SonarCloud token
47+
SONAR_HOST_URL: "https://sonarcloud.io" # Required for SonarCloud
4948
with:
50-
# Additional arguments for the sonarcloud scanner
51-
args:
49+
args: >
5250
-Dsonar.projectKey=startleft
5351
-Dsonar.organization=continuumsec
54-
-Dsonar.python.version=3.9,3.10,3.11
52+
-Dsonar.python.version=3.10,3.11,3.12
5553
-Dsonar.qualitygate.wait=true
5654
-Dsonar.python.coverage.reportPaths=coveragereport/coverage.xml
5755
58-
# Args explanation
59-
# Unique keys of your project and organization. You can find them in SonarCloud > Information (bottom-left menu)
60-
# mandatory
61-
# -Dsonar.projectKey=
62-
# -Dsonar.organization=
56+
# Args explanation
57+
# Unique keys of your project and organization. You can find them in SonarCloud > Information (bottom-left menu)
58+
# mandatory
59+
# -Dsonar.projectKey=
60+
# -Dsonar.organization=
6361

64-
# Version of supported python versions to get a more precise analysis
65-
# -Dsonar.python.version=
62+
# Version of supported python versions to get a more precise analysis
63+
# -Dsonar.python.version=
6664

67-
# Flag to way for Analysis Quality Gate results, if fail the steps it will be marked as failed too.
68-
# -Dsonar.qualitygate.wait=
65+
# Flag to way for Analysis Quality Gate results, if fail the steps it will be marked as failed too.
66+
# -Dsonar.qualitygate.wait=
6967

70-
# The path for coverage report to use in the SonarCloud analysis, it must be in XML format.
71-
# -Dsonar.python.coverage.reportPaths=
68+
# The path for coverage report to use in the SonarCloud analysis, it must be in XML format.
69+
# -Dsonar.python.coverage.reportPaths=

setup.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@
1616
include_package_data=True,
1717
python_requires='>= 3.10, < 3.13',
1818
install_requires=[
19-
'pyyaml==6.0.1',
19+
'pyyaml==6.0.3',
2020
'jsonschema==4.19.0',
2121
'deepmerge==1.1.0',
2222
'jmespath==1.0.1',
2323
'python-hcl2==4.3.2',
2424
'requests==2.32.4',
25-
'fastapi>=0.116.1,<0.117.0',
26-
"python-multipart==0.0.19",
25+
'fastapi>=0.120.4,<0.121.0',
26+
"python-multipart==0.0.20",
2727
'click==8.1.7',
2828
'uvicorn==0.23.2',
2929
'vsdx==0.5.19',

sl_util/sl_util/secure_regex.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,7 @@ def split(pattern, text, maxsplit=0, options=None):
2323

2424
def compile(pattern, options=None):
2525
return re2.compile(pattern, options)
26+
27+
28+
def search(pattern, string, options=None):
29+
return re2.search(pattern, string, options)

sl_util/sl_util/str_utils.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
import html
12
import random
23
import uuid
4+
35
from word2number import w2n
46

7+
from sl_util.sl_util import secure_regex as re
8+
59

610
def deterministic_uuid(source):
711
if source:
@@ -22,5 +26,19 @@ def to_number(input, default_value: int = 0) -> int:
2226
except ValueError:
2327
return default_value
2428

29+
2530
def truncate(s: str, max_length: int) -> str:
26-
return s[:max_length] if s else s
31+
return s[:max_length] if s else s
32+
33+
34+
def remove_html_tags_and_entities(s: str) -> str:
35+
if s is None:
36+
return ''
37+
38+
pattern_tags = re.compile(r'<\s*/?\s*[a-zA-Z]+.*?>')
39+
no_html = re.sub(pattern_tags, ' ', s).strip() if s else s
40+
41+
pattern_spaces = re.compile(r'\s+')
42+
no_spaces = re.sub(pattern_spaces, ' ', no_html) if no_html else no_html
43+
44+
return html.unescape(no_spaces).replace('\xa0', ' ').strip()

sl_util/tests/unit/test_secure_regex_wrapper.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,8 @@ def test_find_all(self, expression, value, expected):
5454
])
5555
def test_split(self, expression, value, expected):
5656
assert sre.findall(expression, value) == expected
57+
58+
59+
def test_search(self):
60+
assert sre.search(r"match\d+.*match\d{1}", "match1 and match2") is not None
61+
assert sre.search(r"match\d+.*match\d{1}", "matchA not found") is None

sl_util/tests/unit/test_str_utils.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
from pytest import mark, param
21
import random
32
from unittest.mock import patch
4-
from sl_util.sl_util.str_utils import deterministic_uuid, to_number
3+
4+
from pytest import mark, param
5+
6+
from sl_util.sl_util.str_utils import deterministic_uuid, to_number, remove_html_tags_and_entities
57

68

79
class TestStrUtils:
@@ -76,3 +78,26 @@ def test_number_conversions_to_alphanumeric(self, source):
7678
number2 = to_number(source)
7779
# Then we obtain default value 0
7880
assert number1 == number2 == 0
81+
82+
@mark.parametrize('source, expected', [
83+
param('<a href="http://example.com">Link</a>', 'Link', id='only link tag'),
84+
param('<p>This is an <b>AWS</b> component.</p>', 'This is an AWS component.', id='with nested tags'),
85+
param('<div><h1>DDBB</h1> <p>Postgres SQL</p></div>', 'DDBB Postgres SQL', id='with multiple nested tags'),
86+
param('< p>This is an <b >AWS</b > component.< /p > <a href="http://example.com">Link</a>',
87+
'This is an AWS component. Link', id='with tags and link'),
88+
param('<p></p>Void tag', 'Void tag', id='void tag'),
89+
param('IN < http & https', 'IN < http & https', id='with lt and ampersand'),
90+
param('OUT > socket & https', 'OUT > socket & https', id='with gt and ampersand'),
91+
param(' 2 < 3 socket > </3 https> <&udp> <=tcp>', '2 < 3 socket > </3 https> <&udp> <=tcp>', id='with non html gt and lt'),
92+
param('No HTML tags here.', 'No HTML tags here.', id='without html tags'),
93+
param('HTML&nbsp;entities&nbsp;&lt;&gt;&amp;&pound;&euro;&copy;', 'HTML entities <>&£€©', id='with html entities'),
94+
param('', '', id='empty string'),
95+
param(None, '', id='null value')
96+
])
97+
def test_remove_html_tags_and_entities(self, source, expected):
98+
# GIVEN a string with html tags
99+
# WHEN removing html tags
100+
result = remove_html_tags_and_entities(source)
101+
102+
# THEN we obtain the expected string
103+
assert result == expected

sl_util/tests/util/file_utils.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,11 @@ def get_upload_file(source: str) -> UploadFile:
1515
tmp_file.seek(0)
1616

1717
return UploadFile(filename=os.path.split(source)[1], file=tmp_file)
18+
19+
20+
def generate_temporary_file(size_in_bytes: int, filename: str = "temp.txt") -> bytes:
21+
temporary_file = SpooledTemporaryFile()
22+
temporary_file.write(b'0' * size_in_bytes)
23+
temporary_file.seek(0)
24+
25+
return UploadFile(filename=filename, file=temporary_file).file.read()

slp_cft/tests/integration/test_cft_processor.py

Lines changed: 141 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22

33
from sl_util.sl_util.file_utils import get_byte_data
44
from slp_base.slp_base.errors import OTMBuildingError, MappingFileNotValidError, IacFileNotValidError, \
5-
LoadingIacFileError
5+
LoadingIacFileError, ErrorCode
6+
from slp_base.slp_base.mapping import MAX_SIZE as MAPPING_MAX_SIZE, MIN_SIZE as MAPPING_MIN_SIZE
67
from slp_base.tests.util.otm import validate_and_compare_otm, validate_and_compare
78
from slp_cft import CloudformationProcessor
89
from slp_cft.tests.resources import test_resource_paths
910
from slp_cft.tests.resources.test_resource_paths import expected_orphan_component_is_not_mapped, \
1011
cft_components_with_trustzones_of_same_type_otm, cloudformation_minimal_content_otm
1112
from slp_cft.tests.utility import excluded_regex
13+
from sl_util.tests.util.file_utils import generate_temporary_file
14+
from slp_cft.slp_cft.validate.cft_validator import MAX_SIZE as FILE_MAX_SIZE, MIN_SIZE as FILE_MIN_SIZE
1215

1316
SAMPLE_ID = 'id'
1417
SAMPLE_NAME = 'name'
@@ -17,8 +20,16 @@
1720
SAMPLE_SINGLE_VALID_CFT_FILE = test_resource_paths.cloudformation_single_file
1821
SAMPLE_VALID_MAPPING_FILE_IR = test_resource_paths.cloudformation_mapping_iriusrisk
1922
SAMPLE_MAPPING_FILE_WITHOUT_REF = test_resource_paths.cloudformation_mapping_without_ref
23+
SAMPLE_DEFAULT_OLD_MAPPING = test_resource_paths.cloudformation_old_default_mapping
24+
SAMPLE_DEFAULT_NEW_MAPPING = test_resource_paths.cloudformation_new_default_mapping
25+
SAMPLE_MAPPING_WITHOUT_TRUSTZONE_TYPE = test_resource_paths.cloudformation_mapping_valid_without_trustzone_type
26+
SAMPLE_CLOUDFORMATION_MAPPING_ALL_FUNCTIONS = test_resource_paths.cloudformation_mapping_all_functions
2027
SAMPLE_NETWORKS_CFT_FILE = test_resource_paths.cloudformation_networks_file
2128
SAMPLE_RESOURCES_CFT_FILE = test_resource_paths.cloudformation_resources_file
29+
SAMPLE_RESOURCES_INVALID_CFT_FILE = test_resource_paths.cloudformation_resources_invalid
30+
SAMPLE_REACT_CORS_SPA_STACK = test_resource_paths.cloudformation_react_cors_spa_stack
31+
SAMPLE_CLOUDFORMATION_ALL_FUNCTIONS = test_resource_paths.cloudformation_all_functions
32+
SAMPLE_CLOUDFORMATION_TEST = test_resource_paths.cloudformation_test
2233
SAMPLE_REF_DEFAULT_JSON = test_resource_paths.cloudformation_with_ref_function_and_default_property_json
2334
SAMPLE_REF_DEFAULT_YAML = test_resource_paths.cloudformation_with_ref_function_and_default_property_yaml
2435
SAMPLE_REF_WITHOUT_DEFAULT_JSON = test_resource_paths.cloudformation_with_ref_function_and_without_default_property_json
@@ -486,7 +497,7 @@ def test_invalid_cloudformation_file(self, cloudformation_file):
486497
mapping_file = [get_byte_data(SAMPLE_VALID_MAPPING_FILE)]
487498

488499
# WHEN creating OTM project from IaC file
489-
# THEN raises OTMBuildingError
500+
# THEN raises IacFileNotValidError
490501
with pytest.raises(IacFileNotValidError):
491502
CloudformationProcessor(SAMPLE_ID, SAMPLE_NAME, cloudformation_file, mapping_file).process()
492503

@@ -522,7 +533,7 @@ def test_run_empty_multiple_iac_files(self):
522533
# GIVEN a request without any iac_file key
523534
mapping_file = get_byte_data(SAMPLE_VALID_MAPPING_FILE_IR)
524535
# WHEN the method CloudformationProcessor::process is invoked
525-
# THEN an RequestValidationError is raised
536+
# THEN an LoadingIacFileError is raised
526537
with pytest.raises(LoadingIacFileError):
527538
CloudformationProcessor('multiple-files', 'multiple-files', [], mapping_file).process()
528539

@@ -541,22 +552,6 @@ def test_security_group_configuration(self, source):
541552
assert len(otm.components) == 1
542553
assert otm.components[0].parent == 'f0ba7722-39b6-4c81-8290-a30a248bb8d9'
543554

544-
def test_multiple_stack_plus_s3_ec2(self):
545-
# GIVEN the file with multiple Subnet AWS::EC2::Instance different configurations
546-
cloudformation_file = get_byte_data(test_resource_paths.multiple_stack_plus_s3_ec2)
547-
# AND a valid iac mappings file
548-
mapping_file = [get_byte_data(SAMPLE_VALID_MAPPING_FILE)]
549-
550-
# WHEN processing
551-
otm = CloudformationProcessor(SAMPLE_ID, SAMPLE_NAME, [cloudformation_file], mapping_file).process()
552-
553-
assert len(otm.components) == 9
554-
publicSubnet1Id = [component for component in otm.components if component.name == 'PublicSubnet1'][0].id
555-
assert publicSubnet1Id
556-
ec2WithWrongParent = [component for component in otm.components if
557-
component.type == 'ec2' and component.parent != publicSubnet1Id]
558-
assert len(ec2WithWrongParent) == 0
559-
560555
def test_parsing_cft_json_file_with_ref(self):
561556
# GIVEN a cloudformation JSON file
562557
cloudformation_file = get_byte_data(SAMPLE_REF_DEFAULT_JSON)
@@ -687,3 +682,130 @@ def test_components_with_trustzones_of_same_type(self):
687682
# THEN the result should be the expected
688683
result, expected = validate_and_compare(otm, cft_components_with_trustzones_of_same_type_otm, None)
689684
assert result == expected
685+
686+
def test_multiple_stack_plus_s3_ec2(self):
687+
# GIVEN the file with multiple Subnet AWS::EC2::Instance different configurations
688+
cloudformation_file = get_byte_data(test_resource_paths.multiple_stack_plus_s3_ec2)
689+
# AND a valid iac mappings file
690+
mapping_file = get_byte_data(SAMPLE_VALID_MAPPING_FILE)
691+
692+
# WHEN processing
693+
otm = CloudformationProcessor(SAMPLE_ID, SAMPLE_NAME, [cloudformation_file], [mapping_file]).process()
694+
695+
assert len(otm.components) == 9
696+
publicSubnet1Id = [component for component in otm.components if component.name == 'PublicSubnet1'][0].id
697+
assert publicSubnet1Id
698+
ec2WithWrongParent = [component for component in otm.components if
699+
component.type == 'ec2' and component.parent != publicSubnet1Id]
700+
assert len(ec2WithWrongParent) == 0
701+
702+
def test_improve_parsing_problems_built_in_functions(self):
703+
# GIVEN a cloudformation file with built-in functions
704+
cloudformation_file = get_byte_data(SAMPLE_REACT_CORS_SPA_STACK)
705+
# AND a valid iac mappings file
706+
mapping_file = get_byte_data(SAMPLE_DEFAULT_OLD_MAPPING)
707+
708+
# WHEN processing
709+
otm = CloudformationProcessor(SAMPLE_ID, SAMPLE_NAME, [cloudformation_file], [mapping_file]).process()
710+
711+
assert len(otm.trustzones) == 1
712+
assert len(otm.dataflows) == 1
713+
assert len(otm.components) == 4
714+
715+
def test_checking_jmespath_functions(self):
716+
# GIVEN a cloudformation file with all JMESPath functions
717+
cloudformation_file = get_byte_data(SAMPLE_CLOUDFORMATION_ALL_FUNCTIONS)
718+
# AND a valid iac mappings file
719+
mapping_file = get_byte_data(SAMPLE_CLOUDFORMATION_MAPPING_ALL_FUNCTIONS)
720+
721+
# WHEN processing
722+
otm = CloudformationProcessor(SAMPLE_ID, SAMPLE_NAME, [cloudformation_file], [mapping_file]).process()
723+
724+
assert len(otm.trustzones) == 1
725+
assert len(otm.dataflows) == 0
726+
assert len(otm.components) == 5
727+
728+
def test_not_present_parents(self):
729+
# GIVEN a cloudformation file with all JMESPath functions
730+
cloudformation_file = get_byte_data(SAMPLE_CLOUDFORMATION_TEST)
731+
# AND a valid iac mappings file
732+
mapping_file = get_byte_data(SAMPLE_DEFAULT_NEW_MAPPING)
733+
734+
# WHEN processing
735+
otm = CloudformationProcessor(SAMPLE_ID, SAMPLE_NAME, [cloudformation_file], [mapping_file]).process()
736+
737+
assert len(otm.trustzones) == 1
738+
assert len(otm.dataflows) == 0
739+
assert len(otm.components) == 4
740+
741+
def test_invalid_resources_mapping_file(self):
742+
# GIVEN a valid CFT file with altsource resources
743+
cloudformation_file = get_byte_data(SAMPLE_VALID_CFT_FILE)
744+
745+
# AND a invalid format CFT mapping file
746+
mapping_file = get_byte_data(SAMPLE_RESOURCES_INVALID_CFT_FILE)
747+
748+
# WHEN the CFT file is processed
749+
# THEN an MappingFileNotValidError is raised
750+
with pytest.raises(MappingFileNotValidError) as error:
751+
CloudformationProcessor(SAMPLE_ID, SAMPLE_NAME, [cloudformation_file], [mapping_file]).process()
752+
753+
# AND the error details are correct
754+
assert ErrorCode.MAPPING_FILE_NOT_VALID == error.value.error_code
755+
assert 'Mapping files are not valid' == error.value.title
756+
assert 'Mapping file does not comply with the schema' == error.value.detail
757+
assert "'trustzones' is a required property" == error.value.message
758+
759+
@pytest.mark.parametrize('cft_file_size', [FILE_MAX_SIZE + 1, FILE_MIN_SIZE - 1])
760+
def test_min_max_cloudformation_file_sizes(self, cft_file_size):
761+
# GIVEN a max file size limit and a valid CFT file
762+
max_file_size_allowed_in_bytes = 1024 * 1024
763+
cloudformation_file = generate_temporary_file(cft_file_size, "test_max_size.txt")
764+
765+
# AND a valid CFT mapping file
766+
mapping_file = get_byte_data(SAMPLE_VALID_MAPPING_FILE)
767+
768+
# WHEN the CFT file is processed
769+
# THEN an IacFileNotValidError is raised
770+
with pytest.raises(IacFileNotValidError) as error:
771+
CloudformationProcessor(SAMPLE_ID, SAMPLE_NAME, [cloudformation_file], [mapping_file]).process()
772+
773+
# AND the error details are correct
774+
assert ErrorCode.IAC_NOT_VALID == error.value.error_code
775+
assert 'CloudFormation file is not valid' == error.value.title
776+
assert 'Provided iac_file is not valid. Invalid size' == error.value.detail
777+
assert 'Provided iac_file is not valid. Invalid size' == error.value.message
778+
779+
@pytest.mark.parametrize('mapping_file_size', [MAPPING_MAX_SIZE + 1, MAPPING_MIN_SIZE - 1])
780+
def test_min_max_mapping_file_sizes(self, mapping_file_size):
781+
# GIVEN a valid CFT file with altsource resources
782+
cloudformation_file = get_byte_data(SAMPLE_VALID_CFT_FILE)
783+
784+
# AND a invalid size CFT mapping file
785+
mapping_file = generate_temporary_file(mapping_file_size, "test_mapping_sizes.txt")
786+
787+
# WHEN the CFT file is processed
788+
# THEN an MappingFileNotValidError is raised
789+
with pytest.raises(MappingFileNotValidError) as error:
790+
CloudformationProcessor(SAMPLE_ID, SAMPLE_NAME, [cloudformation_file], [mapping_file]).process()
791+
792+
# AND the error details are correct
793+
assert ErrorCode.MAPPING_FILE_NOT_VALID == error.value.error_code
794+
assert 'Mapping files are not valid' == error.value.title
795+
assert 'Mapping files are not valid. Invalid size' == error.value.detail
796+
assert 'Mapping files are not valid. Invalid size' == error.value.message
797+
798+
def test_mapping_trustzone_no_type(self):
799+
# GIVEN a valid CFT file with some resources
800+
cloudformation_file = get_byte_data(test_resource_paths.cloudformation_for_security_group_tests_json)
801+
802+
# AND a valid CFT mapping file
803+
mapping_file = get_byte_data(SAMPLE_MAPPING_WITHOUT_TRUSTZONE_TYPE)
804+
805+
# WHEN the CFT file is processed
806+
otm = CloudformationProcessor(SAMPLE_ID, SAMPLE_NAME, [cloudformation_file], [mapping_file]).process()
807+
808+
# THEN the number of TZs, components and dataflows are right
809+
assert len(otm.trustzones) == 2
810+
assert len(otm.components) == 22
811+
assert len(otm.dataflows) == 22

0 commit comments

Comments
 (0)