Skip to content

Commit da1f990

Browse files
authored
#81 add security scan command (#96)
Fixes #81 Co-authors @tkilias
1 parent b656c33 commit da1f990

File tree

12 files changed

+266
-3
lines changed

12 files changed

+266
-3
lines changed

doc/changes/changes_0.7.0.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ t.b.d.
99
## Features / Enhancements
1010

1111
- #90: Improve test performance
12+
- #81: Add security scan command
1213

1314
## Bug Fixes
1415

exasol_script_languages_container_tool/cli/commands/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@
66
from .run_db_tests import run_db_test
77
from .save import save
88
from .upload import upload
9+
from .security_scan import security_scan
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from pathlib import Path
2+
from typing import Tuple
3+
4+
from exasol_integration_test_docker_environment.cli.cli import cli
5+
from exasol_integration_test_docker_environment.cli.common import add_options, import_build_steps, set_build_config, \
6+
set_docker_repository_config, generate_root_task, run_task
7+
from exasol_integration_test_docker_environment.cli.options.build_options import build_options
8+
from exasol_integration_test_docker_environment.cli.options.docker_repository_options import docker_repository_options
9+
from exasol_integration_test_docker_environment.cli.options.system_options import system_options
10+
11+
from exasol_script_languages_container_tool.cli.options.flavor_options import flavor_options
12+
from exasol_script_languages_container_tool.lib.tasks.security_scan.security_scan import SecurityScan
13+
from exasol_script_languages_container_tool.lib.utils.logging_redirection import log_redirector_task_creator_wrapper
14+
15+
16+
@cli.command()
17+
@add_options(flavor_options)
18+
@add_options(build_options)
19+
@add_options(docker_repository_options)
20+
@add_options(system_options)
21+
def security_scan(flavor_path: Tuple[str, ...],
22+
force_rebuild: bool,
23+
force_rebuild_from: Tuple[str, ...],
24+
force_pull: bool,
25+
output_directory: str,
26+
temporary_base_directory: str,
27+
log_build_context_content: bool,
28+
cache_directory: str,
29+
build_name: str,
30+
source_docker_repository_name: str,
31+
source_docker_tag_prefix: str,
32+
source_docker_username: str,
33+
source_docker_password: str,
34+
target_docker_repository_name: str,
35+
target_docker_tag_prefix: str,
36+
target_docker_username: str,
37+
target_docker_password: str,
38+
workers: int,
39+
task_dependencies_dot_file: str):
40+
"""
41+
This command executes the security scan, which must be defined as separate step in the build steps declaration.
42+
The scan runs the docker container of the respective step, passing a folder of the output-dir as argument.
43+
If the stages do not exists locally, the system will build or pull them before running the scan.
44+
"""
45+
import_build_steps(flavor_path)
46+
set_build_config(force_rebuild,
47+
force_rebuild_from,
48+
force_pull,
49+
log_build_context_content,
50+
output_directory,
51+
temporary_base_directory,
52+
cache_directory,
53+
build_name)
54+
set_docker_repository_config(source_docker_password, source_docker_repository_name, source_docker_username,
55+
source_docker_tag_prefix, "source")
56+
set_docker_repository_config(target_docker_password, target_docker_repository_name, target_docker_username,
57+
target_docker_tag_prefix, "target")
58+
59+
report_path = Path(output_directory).joinpath("security_scan")
60+
task_creator = log_redirector_task_creator_wrapper(lambda: generate_root_task(task_class=SecurityScan,
61+
flavor_paths=list(flavor_path),
62+
report_path=report_path
63+
))
64+
65+
success, task = run_task(task_creator, workers, task_dependencies_dot_file)
66+
67+
if success:
68+
with task.security_report_target.open("r") as f:
69+
print(f.read())
70+
print(f'Full security scan report can be found at:{report_path}')
71+
if not success:
72+
exit(1)

exasol_script_languages_container_tool/lib/tasks/security_scan/__init__.py

Whitespace-only changes.
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
from pathlib import Path
2+
from typing import Dict
3+
4+
import luigi
5+
6+
import tarfile
7+
8+
from exasol_integration_test_docker_environment.lib.base.flavor_task import FlavorsBaseTask
9+
from exasol_integration_test_docker_environment.lib.config.build_config import build_config
10+
from exasol_integration_test_docker_environment.lib.docker import ContextDockerClient
11+
12+
from exasol_script_languages_container_tool.lib.tasks.build.docker_flavor_build_base import DockerFlavorBuildBase
13+
14+
from exasol_script_languages_container_tool.lib.tasks.security_scan.security_scan_parameter import SecurityScanParameter
15+
16+
from docker.models.containers import Container
17+
18+
19+
class ScanResult:
20+
def __init__(self, is_ok: bool, summary: str, report_dir: Path):
21+
self.is_ok = is_ok
22+
self.summary = summary
23+
self.report_dir = report_dir
24+
25+
26+
class SecurityScan(FlavorsBaseTask, SecurityScanParameter):
27+
28+
def __init__(self, *args, **kwargs):
29+
self.security_scanner_futures = None
30+
super().__init__(*args, **kwargs)
31+
report_path = self.report_path.joinpath("security_report")
32+
self.security_report_target = luigi.LocalTarget(str(report_path))
33+
34+
def register_required(self):
35+
tasks = self.create_tasks_for_flavors_with_common_params(
36+
SecurityScanner, report_path=self.report_path) # type: Dict[str,SecurityScanner]
37+
self.security_scanner_futures = self.register_dependencies(tasks)
38+
39+
def run_task(self):
40+
security_scanner_results = self.get_values_from_futures(
41+
self.security_scanner_futures)
42+
43+
self.write_report(security_scanner_results)
44+
all_result = AllScanResult(security_scanner_results)
45+
if not all_result.scans_are_ok:
46+
raise RuntimeError(f"Not all security scans were successful.:\n{all_result.get_error_scans_msg()}")
47+
48+
def write_report(self, security_scanner: Dict[str, ScanResult]):
49+
with self.security_report_target.open("w") as out_file:
50+
51+
for key, value in security_scanner.items():
52+
out_file.write("\n")
53+
out_file.write(f"============ START SECURITY SCAN REPORT - <{key}> ====================")
54+
out_file.write("\n")
55+
out_file.write(f"Successful:{value.is_ok}\n")
56+
out_file.write(f"Full report:{value.report_dir}\n")
57+
out_file.write(f"Summary:\n")
58+
out_file.write(value.summary)
59+
out_file.write("\n")
60+
out_file.write(f"============ END SECURITY SCAN REPORT - <{key}> ====================")
61+
out_file.write("\n")
62+
63+
64+
class SecurityScanner(DockerFlavorBuildBase, SecurityScanParameter):
65+
66+
def get_goals(self):
67+
return {"security_scan"}
68+
69+
def get_release_task(self):
70+
return self.create_build_tasks(not build_config().force_rebuild)
71+
72+
def run_task(self):
73+
tasks = self.get_release_task()
74+
75+
tasks_futures = yield from self.run_dependencies(tasks)
76+
task_results = self.get_values_from_futures(tasks_futures)
77+
flavor_path = Path(self.flavor_path)
78+
report_path = self.report_path.joinpath(flavor_path.name)
79+
report_path.mkdir(parents=True, exist_ok=True)
80+
report_path_abs = report_path.absolute()
81+
result = ScanResult(is_ok=False, summary="", report_dir=report_path_abs)
82+
assert len(task_results.values()) == 1
83+
for task_result in task_results.values():
84+
self.logger.info(f"Running security run on image: {task_result.get_target_complete_name()}, report path: "
85+
f"{report_path_abs}")
86+
87+
report_local_path = "/report"
88+
with ContextDockerClient() as docker_client:
89+
result_container = docker_client.containers.run(task_result.get_target_complete_name(),
90+
command=report_local_path,
91+
detach=True, stderr=True)
92+
try:
93+
logs = result_container.logs(follow=True).decode("UTF-8")
94+
result_container_result = result_container.wait()
95+
#We don't use mount binding here to exchange the report files, but download them from the container
96+
#Thus we avoid that the files are created by root
97+
self._write_report(result_container, report_path_abs, report_local_path)
98+
result = ScanResult(is_ok=(result_container_result["StatusCode"] == 0),
99+
summary=logs, report_dir=report_path_abs)
100+
finally:
101+
result_container.remove()
102+
103+
self.return_object(result)
104+
105+
def _write_report(self, container: Container, report_path_abs: Path, report_local_path: str):
106+
tar_file_path = report_path_abs / 'report.tar'
107+
with open(tar_file_path, 'wb') as tar_file:
108+
bits, stat = container.get_archive(report_local_path)
109+
for chunk in bits:
110+
tar_file.write(chunk)
111+
with tarfile.open(tar_file_path) as tar_file:
112+
tar_file.extractall(path=report_path_abs)
113+
114+
115+
class AllScanResult:
116+
def __init__(self, scan_results_per_flavor: Dict[str, ScanResult]):
117+
self.scan_results_per_flavor = scan_results_per_flavor
118+
self.scans_are_ok = all(scan_result.is_ok
119+
for scan_result
120+
in scan_results_per_flavor.values())
121+
122+
def get_error_scans_msg(self):
123+
return [f"{key}: '{value.summary}'" for key, value in self.scan_results_per_flavor.items() if not value.is_ok]
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import luigi
2+
from exasol_integration_test_docker_environment.lib.base.dependency_logger_base_task import DependencyLoggerBaseTask
3+
4+
5+
class SecurityScanParameter(DependencyLoggerBaseTask):
6+
report_path = luigi.Parameter()

exasol_script_languages_container_tool/main.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
build_test_container
66

77
from exasol_script_languages_container_tool.cli.commands import build, clean_all_images, clean_flavor_images, export, \
8-
generate_language_activation, push, run_db_test, save, upload, clean
8+
generate_language_activation, push, run_db_test, save, upload, clean, security_scan
99

1010
if __name__ == '__main__':
1111
# Required to announce the commands to click
@@ -21,5 +21,6 @@
2121
run_db_test,
2222
save,
2323
upload,
24-
clean]
24+
clean,
25+
security_scan]
2526
cli()

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
'exasol_script_languages_container_tool.lib.tasks.export',
1414
'exasol_script_languages_container_tool.lib.tasks.push',
1515
'exasol_script_languages_container_tool.lib.tasks.save',
16+
'exasol_script_languages_container_tool.lib.tasks.security_scan',
1617
'exasol_script_languages_container_tool.lib.tasks.test',
1718
'exasol_script_languages_container_tool.lib.tasks.upload',
1819
'exasol_script_languages_container_tool.lib.utils']

test/resources/real-test-flavor/real_flavor_base/build_steps.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,15 +122,27 @@ def requires_tasks(self):
122122
def get_path_in_flavor(self):
123123
return "flavor_base"
124124

125-
126125
class AnalyzeRelease(DockerFlavorAnalyzeImageTask):
127126
def get_build_step(self) -> str:
128127
return "release"
129128

130129
def requires_tasks(self):
131130
return {"flavor_customization": AnalyzeFlavorCustomization,
132131
"build_run": AnalyzeBuildRun,
132+
"language_deps": AnalyzeLanguageDeps,
133133
"language_deps": AnalyzeLanguageDeps}
134134

135135
def get_path_in_flavor(self):
136136
return "flavor_base"
137+
138+
139+
class SecurityScan(DockerFlavorAnalyzeImageTask):
140+
def get_build_step(self) -> str:
141+
return "security_scan"
142+
143+
def requires_tasks(self):
144+
return {"release": AnalyzeRelease}
145+
146+
def get_path_in_flavor(self):
147+
return "flavor_base"
148+
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
FROM {{release}}
2+
3+
RUN echo "Building security scan..."
4+
5+
COPY security_scan/test.sh /test.sh
6+
ENTRYPOINT ["/test.sh"]

0 commit comments

Comments
 (0)