Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
082730c
CM-25773- initial commit
EfratIsrael Aug 1, 2023
12b8a5c
lint fix
EfratIsrael Aug 1, 2023
a09ff32
remove new line
EfratIsrael Aug 1, 2023
197e459
add try-except
EfratIsrael Aug 1, 2023
14d1c52
change list to List
EfratIsrael Aug 1, 2023
c03bb27
key error exception handler
EfratIsrael Aug 1, 2023
11ca913
prettier
EfratIsrael Aug 1, 2023
84242b9
encoding
EfratIsrael Aug 1, 2023
d13d518
fix path
EfratIsrael Aug 1, 2023
803c275
lint
EfratIsrael Aug 1, 2023
d32568d
review fixing
EfratIsrael Aug 2, 2023
763622e
renaming + typo fixing
EfratIsrael Aug 2, 2023
2bf1ded
remove redundant json load check
EfratIsrael Aug 2, 2023
7bda660
change soft fail
EfratIsrael Aug 2, 2023
f8d4933
add json.loads wrapper
EfratIsrael Aug 2, 2023
046b08d
lint
EfratIsrael Aug 2, 2023
a8050ea
remove constructor
EfratIsrael Aug 2, 2023
184c9d8
refactor iac parsing
EfratIsrael Aug 2, 2023
97f1b03
handle null after + add test
EfratIsrael Aug 3, 2023
7c3296a
Fix error handling
MarshalX Aug 3, 2023
f9042bf
Fix replacement of files
MarshalX Aug 3, 2023
f2a5ea5
fix iac doc manipulation
EfratIsrael Aug 3, 2023
d4cd8f9
revert iac manipultaion outside pre scan doc
EfratIsrael Aug 3, 2023
1d6a90a
adding tests for different plans +
EfratIsrael Aug 3, 2023
f0249e3
fix readme
EfratIsrael Aug 3, 2023
27d4505
fix readme
EfratIsrael Aug 3, 2023
dab3f92
fixing
EfratIsrael Aug 3, 2023
4c3784c
pr fixing
EfratIsrael Aug 6, 2023
1e1ac2d
pr fixing
EfratIsrael Aug 6, 2023
9ab3334
lint
EfratIsrael Aug 6, 2023
921b047
test fix
EfratIsrael Aug 6, 2023
805b0f6
add test for generate document
EfratIsrael Aug 6, 2023
b68e68f
lint
EfratIsrael Aug 6, 2023
fa0196e
update test
EfratIsrael Aug 7, 2023
cfb3b88
typo fix
EfratIsrael Aug 7, 2023
b0435d6
move \n
EfratIsrael Aug 7, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions cycode/cli/code_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@
from cycode.cli.ci_integrations import get_commit_range
from cycode.cli.config import configuration_manager
from cycode.cli.exceptions import custom_exceptions
from cycode.cli.helpers import sca_code_scanner
from cycode.cli.helpers import sca_code_scanner, tf_content_generator
from cycode.cli.models import CliError, CliErrors, Document, DocumentDetections, LocalScanResult, Severity
from cycode.cli.printers import ConsolePrinter
from cycode.cli.user_settings.config_file_manager import ConfigFileManager
from cycode.cli.utils import scan_utils
from cycode.cli.utils.file_utils import change_filename_extension
from cycode.cli.utils.path_utils import (
get_file_content,
get_file_size,
Expand Down Expand Up @@ -336,7 +337,19 @@ def scan_disk_files(context: click.Context, path: str, files_to_scan: List[str])
if not content:
continue

documents.append(Document(file, content, is_git_diff))
file_name = file

if _is_iac(scan_type) and _is_tfplan_json_file(file, content):
try:
content = tf_content_generator.generate_tf_content_from_tfplan(content)
except KeyError:
_handle_exception(
context,
custom_exceptions.TfplanKeyError('Error occurred while parsing tfplan file.'),
)
file_name = change_filename_extension(file, 'tf')

documents.append(Document(file_name, content, is_git_diff))

perform_pre_scan_documents_actions(context, scan_type, documents, is_git_diff)
scan_documents(context, documents, is_git_diff=is_git_diff, scan_parameters=scan_parameters)
Expand Down Expand Up @@ -1099,6 +1112,21 @@ def _is_file_extension_supported(scan_type: str, filename: str) -> bool:
return not filename.endswith(consts.SECRET_SCAN_FILE_EXTENSIONS_TO_IGNORE)


def _is_iac(scan_type: str) -> bool:
return scan_type == consts.INFRA_CONFIGURATION_SCAN_TYPE


def _is_tfplan_json_file(file: str, content: str) -> bool:
if not file.endswith('.json'):
return False
try:
tf_plan = json.loads(content)
return tf_plan.get('resource_changes')

except ValueError:
return False


def _does_file_exceed_max_size_limit(filename: str) -> bool:
return get_file_size(filename) > consts.FILE_MAX_SIZE_LIMIT_IN_BYTES

Expand Down Expand Up @@ -1157,6 +1185,13 @@ def _handle_exception(context: click.Context, e: Exception, *, return_exception:
'Please try ignoring irrelevant paths using the `cycode ignore --by-path` command '
'and execute the scan again',
),
custom_exceptions.TfplanKeyError: CliError(
soft_fail=True,
code='key_error',
message='A crucial field is missing in you terraform plan file. '
'Please make sure that your file is well formed '
'and execute the scan again',
),
InvalidGitRepositoryError: CliError(
soft_fail=False,
code='invalid_git_error',
Expand Down
6 changes: 6 additions & 0 deletions cycode/cli/exceptions/custom_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,9 @@ def __init__(self, error_message: str) -> None:

def __str__(self) -> str:
return f'Something went wrong during the authentication process, error message: {self.error_message}'


class TfplanKeyError(CycodeError):
def __init__(self, error_message: str) -> None:
self.error_message = error_message
super().__init__()
29 changes: 29 additions & 0 deletions cycode/cli/helpers/tf_content_generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import json
from typing import List

from cycode.cli.models import ChangeResource


def generate_tf_content_from_tfplan(tfplan: str) -> str:
planned_resources = _extract_resources(tfplan)
return _generate_tf_content(planned_resources)


def _extract_resources(tfplan: str) -> List[ChangeResource]:
tfplan_json = json.loads(tfplan)
resources: List[ChangeResource] = []
for change in tfplan_json.get('resource_changes', []):
resources.append(
ChangeResource(resource_type=change['type'], name=change['name'], values=change['change']['after'])
)
return resources


def _generate_tf_content(resources: List[ChangeResource]) -> str:
tf_content = ''
for resource in resources:
tf_content += f'resource \"{resource.resource_type}\" \"{resource.name}\" {{\n'
for key, value in resource.values.items():
tf_content += f' {key} = {json.dumps(value)}\n'
tf_content += '}\n\n'
return tf_content
7 changes: 7 additions & 0 deletions cycode/cli/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,10 @@ class LocalScanResult(NamedTuple):
issue_detected: bool
detections_count: int
relevant_detections_count: int


class ChangeResource:
def __init__(self, resource_type: str, name: str, values: Dict[str, any]) -> None:
self.resource_type = resource_type
self.name = name
self.values = values
6 changes: 6 additions & 0 deletions cycode/cli/utils/file_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import os


def change_filename_extension(filename: str, extension: str) -> str:
base_name, old_ext = os.path.splitext(filename)
return base_name + '.' + extension
Empty file added tests/cli/helpers/__init__.py
Empty file.
21 changes: 21 additions & 0 deletions tests/cli/helpers/test_tf_content_generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import os

from cycode.cli.helpers import tf_content_generator

examples_main_dir = '../../test_files/tf_content_generator_files'


def test_generate_tf_content_from_tfplan() -> None:
examples_directories = [
name for name in os.listdir(examples_main_dir) if os.path.isdir(os.path.join(examples_main_dir, name))
]

for example in examples_directories:
tfplan_path = os.path.join(examples_main_dir, example, 'tfplan.json')
tf_expected_content_path = os.path.join(examples_main_dir, example, 'tf_content.txt')
with open(tfplan_path, 'r') as tfplan_file, open(tf_expected_content_path, 'r') as tf_expected_content_file:
tfplan_content: str = tfplan_file.read()
tf_expected_content: str = tf_expected_content_file.read()
tf_content = tf_content_generator.generate_tf_content_from_tfplan(tfplan_content)
assert tf_content == tf_expected_content

1 change: 1 addition & 0 deletions tests/cli/test_code_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def ctx() -> click.Context:
(custom_exceptions.ScanAsyncError('msg'), True),
(custom_exceptions.HttpUnauthorizedError('msg', Response()), True),
(custom_exceptions.ZipTooLargeError(1000), True),
(custom_exceptions.TfplanKeyError('msg'), True),
(InvalidGitRepositoryError(), None),
],
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
resource "aws_s3_bucket" "efrat-env-var-test" {
bucket = "efrat-env-var-test"
force_destroy = false
tags = null
timeouts = null
}

resource "aws_s3_bucket_public_access_block" "efrat-env-var-test" {
block_public_acls = false
block_public_policy = true
ignore_public_acls = false
restrict_public_buckets = true
}

218 changes: 218 additions & 0 deletions tests/test_files/tf_content_generator_files/example_1/tfplan.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
{
"format_version": "1.2",
"terraform_version": "1.5.4",
"variables": {
"IT_IS_FALSE": {
"value": "false"
}
},
"planned_values": {
"root_module": {
"resources": [
{
"address": "aws_s3_bucket.efrat-env-var-test",
"mode": "managed",
"type": "aws_s3_bucket",
"name": "efrat-env-var-test",
"provider_name": "registry.terraform.io/hashicorp/aws",
"schema_version": 0,
"values": {
"bucket": "efrat-env-var-test",
"force_destroy": false,
"tags": null,
"timeouts": null
},
"sensitive_values": {
"cors_rule": [],
"grant": [],
"lifecycle_rule": [],
"logging": [],
"object_lock_configuration": [],
"replication_configuration": [],
"server_side_encryption_configuration": [],
"tags_all": {},
"versioning": [],
"website": []
}
},
{
"address": "aws_s3_bucket_public_access_block.efrat-env-var-test",
"mode": "managed",
"type": "aws_s3_bucket_public_access_block",
"name": "efrat-env-var-test",
"provider_name": "registry.terraform.io/hashicorp/aws",
"schema_version": 0,
"values": {
"block_public_acls": false,
"block_public_policy": true,
"ignore_public_acls": false,
"restrict_public_buckets": true
},
"sensitive_values": {}
}
]
}
},
"resource_changes": [
{
"address": "aws_s3_bucket.efrat-env-var-test",
"mode": "managed",
"type": "aws_s3_bucket",
"name": "efrat-env-var-test",
"provider_name": "registry.terraform.io/hashicorp/aws",
"change": {
"actions": [
"create"
],
"before": null,
"after": {
"bucket": "efrat-env-var-test",
"force_destroy": false,
"tags": null,
"timeouts": null
},
"after_unknown": {
"acceleration_status": true,
"acl": true,
"arn": true,
"bucket_domain_name": true,
"bucket_prefix": true,
"bucket_regional_domain_name": true,
"cors_rule": true,
"grant": true,
"hosted_zone_id": true,
"id": true,
"lifecycle_rule": true,
"logging": true,
"object_lock_configuration": true,
"object_lock_enabled": true,
"policy": true,
"region": true,
"replication_configuration": true,
"request_payer": true,
"server_side_encryption_configuration": true,
"tags_all": true,
"versioning": true,
"website": true,
"website_domain": true,
"website_endpoint": true
},
"before_sensitive": false,
"after_sensitive": {
"cors_rule": [],
"grant": [],
"lifecycle_rule": [],
"logging": [],
"object_lock_configuration": [],
"replication_configuration": [],
"server_side_encryption_configuration": [],
"tags_all": {},
"versioning": [],
"website": []
}
}
},
{
"address": "aws_s3_bucket_public_access_block.efrat-env-var-test",
"mode": "managed",
"type": "aws_s3_bucket_public_access_block",
"name": "efrat-env-var-test",
"provider_name": "registry.terraform.io/hashicorp/aws",
"change": {
"actions": [
"create"
],
"before": null,
"after": {
"block_public_acls": false,
"block_public_policy": true,
"ignore_public_acls": false,
"restrict_public_buckets": true
},
"after_unknown": {
"bucket": true,
"id": true
},
"before_sensitive": false,
"after_sensitive": {}
}
}
],
"configuration": {
"provider_config": {
"aws": {
"name": "aws",
"full_name": "registry.terraform.io/hashicorp/aws",
"expressions": {
"profile": {
"constant_value": "efrat"
},
"region": {
"constant_value": "us-east-1"
}
}
}
},
"root_module": {
"resources": [
{
"address": "aws_s3_bucket.efrat-env-var-test",
"mode": "managed",
"type": "aws_s3_bucket",
"name": "efrat-env-var-test",
"provider_config_key": "aws",
"expressions": {
"bucket": {
"constant_value": "efrat-env-var-test"
}
},
"schema_version": 0
},
{
"address": "aws_s3_bucket_public_access_block.efrat-env-var-test",
"mode": "managed",
"type": "aws_s3_bucket_public_access_block",
"name": "efrat-env-var-test",
"provider_config_key": "aws",
"expressions": {
"block_public_acls": {
"references": [
"var.IT_IS_FALSE"
]
},
"block_public_policy": {
"constant_value": true
},
"bucket": {
"references": [
"aws_s3_bucket.efrat-env-var-test.id",
"aws_s3_bucket.efrat-env-var-test"
]
},
"ignore_public_acls": {
"constant_value": false
},
"restrict_public_buckets": {
"constant_value": true
}
},
"schema_version": 0
}
],
"variables": {
"IT_IS_FALSE": {
"description": "This is an example input variable using env variables."
}
}
}
},
"relevant_attributes": [
{
"resource": "aws_s3_bucket.efrat-env-var-test",
"attribute": [
"id"
]
}
],
"timestamp": "2023-07-31T17:54:18Z"
}