Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
0314d9f
adding composer.lock download functionality to trivy sca
faizanH May 12, 2025
df03719
add test case
faizanH May 12, 2025
627c5e8
formatting
faizanH May 12, 2025
e96fe48
updating dind to have php and curl
faizanH May 12, 2025
deeaae7
adding required packages for PHP
faizanH May 12, 2025
8dcd3fe
adding one more PHP requirement
faizanH May 12, 2025
18ac459
trying a multi stage build
faizanH May 12, 2025
47ec973
testing
faizanH May 12, 2025
8fafd6c
adding more php depdencies
faizanH May 12, 2025
0416d29
removing incorrect package
faizanH May 13, 2025
7dc3078
removing improper import
faizanH May 13, 2025
8d62552
invoke the script
faizanH May 13, 2025
bf65480
attempting to create docker container in plugin directly
faizanH May 14, 2025
ac6c8f8
remove php from dockerfile
faizanH May 14, 2025
bce7432
adding config in settings.py
faizanH May 14, 2025
f165e10
adding logs for debugging
faizanH May 14, 2025
dfe927a
commenting out return
faizanH May 14, 2025
acb39fb
trying to get logs to appear to show file directory
faizanH May 16, 2025
880247c
Merge branch 'main' into feature/generate-php-lockfile
faizanH May 16, 2025
32ab4e2
tring to enter container
faizanH May 16, 2025
b849677
slight tweaks
faizanH May 23, 2025
a60c99d
debug
faizanH May 23, 2025
22eb123
adding more debugs
faizanH May 27, 2025
4ebbe82
remove possible error causing
faizanH May 27, 2025
83b314c
adding more debugging
faizanH May 27, 2025
14eac8c
using different temp volume
faizanH May 27, 2025
8cab6cf
trying different approach
faizanH May 28, 2025
2107bcf
drawing inspo from other plugin
faizanH May 29, 2025
d8e3de7
Merge branch 'main' into feature/generate-php-lockfile
faizanH Aug 27, 2025
83de2b2
debugging
mdfleury-wbd Aug 27, 2025
9d9750a
testing working dir
mdfleury-wbd Aug 28, 2025
d21e59c
changing path again
mdfleury-wbd Aug 28, 2025
1e73976
another try
mdfleury-wbd Aug 28, 2025
0d26b9b
adding missed file
mdfleury-wbd Aug 28, 2025
3b53779
renaming npm file
mdfleury-wbd Aug 28, 2025
c077fba
using mnt
mdfleury-wbd Aug 28, 2025
76be831
trying working mnt
mdfleury-wbd Sep 3, 2025
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
101 changes: 101 additions & 0 deletions backend/engine/plugins/lib/trivy_common/generate_composer_locks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import os
from glob import glob
from engine.plugins.lib import utils
import docker
import uuid
from typing import Optional

logger = utils.setup_logging("trivy_sca")
docker_client = docker.from_env()


def install_package_files(path: str, include_dev: bool, sub_path: str, working_src: str, root_path: str):
# sub_path: absolute path to the composer project inside the parent container (e.g. /tmp/work/foo/bar)
# temp_vol_name: Docker volume name (e.g. artemis-plugin-temp-xxxx)
# temp_vol_mount: mount path inside the plugin container (e.g. /tmp/work)
# root_path: the original root for logging

rel_subdir = os.path.relpath(sub_path, path)
abs_path_in_container = os.path.join("/app", rel_subdir)
logger.info(f"Mounting volume: {working_src} to /app in composer container")
logger.info(f"Target subdir in container: {abs_path_in_container}")
logger.info(f"composer.json: {os.path.join(sub_path, 'composer.json')}")
logger.info(f"composer.json exists: {os.path.exists(os.path.join(sub_path, 'composer.json'))}")

composer_cmd = (
"composer --version && "
"ls -l && "
"cat composer.json && "
"composer install --no-scripts -q"
" && ls -l composer.lock && ls -l"
)

# if not include_dev:
# composer_cmd += " --no-dev"

COMPOSER_IMG = "composer:2.8.11"
container_name = f"composer_runner_{uuid.uuid4().hex[:8]}"
container_mount_path = "/app"

try:
container = docker_client.containers.run(
COMPOSER_IMG,
name=container_name,
command=["sh", "-c", composer_cmd],
volumes={
working_src: {"bind": container_mount_path, "mode": "rw"},
},
working_dir=abs_path_in_container,
auto_remove=False,
stdout=True,
stderr=True,
detach=True,
)

result = container.wait()
logs = container.logs(stdout=True, stderr=True).decode("utf-8")
logger.info(f"Container logs for {sub_path.replace(root_path, '')}:\n{logs}")
logger.info(f"Container exit code: {result.get('StatusCode')}")
container.remove()
except Exception as e:
logger.error(f"Error running composer install in Docker: {e}")

# Check if composer.lock was created
lockfile = os.path.join(sub_path, "composer.lock")
if not os.path.exists(lockfile):
logger.error(f"composer.lock was not created in {sub_path}")

return


def check_composer_package_files(
path: str, working_src: str, include_dev: bool, root_path: Optional[str] = None
) -> tuple:
"""
Find all composer.json files in the repo and build lock files for them if missing.
"""
errors = []
alerts = []
logger.info("Searching %s for composer files", path)
files = glob(f"{path}/**/composer.json", recursive=True)
logger.info("Found %d composer.json files", len(files))

if len(files) == 0:
return errors, alerts

paths = set()
for filename in files:
paths.add(os.path.dirname(filename))

for sub_path in paths:
lockfile = os.path.join(sub_path, "composer.lock")
lockfile_missing = not os.path.exists(lockfile)
if lockfile_missing:
msg = (
f"No composer.lock file was found in path {sub_path.replace(path, '')}."
" Please consider creating a composer.lock file for this project."
)
logger.warning(msg)
alerts.append(msg)
install_package_files(path, include_dev, sub_path, working_src, root_path or working_src)
return errors, alerts
2 changes: 1 addition & 1 deletion backend/engine/plugins/trivy_sbom/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import json
import subprocess
from typing import Optional
from engine.plugins.lib.trivy_common.generate_locks import check_package_files
from engine.plugins.lib.trivy_common.generate_npm_locks import check_package_files
from engine.plugins.lib.sbom_common.go_installer import go_mod_download
from engine.plugins.trivy_sbom.parser import clean_output_application_sbom
from engine.plugins.trivy_sbom.parser import edit_application_sbom_path
Expand Down
29 changes: 21 additions & 8 deletions backend/engine/plugins/trivy_sca/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
trivy SCA plugin
"""

from os.path import abspath

import json
import subprocess
from engine.plugins.lib.trivy_common.generate_locks import check_package_files
from engine.plugins.lib.trivy_common.generate_npm_locks import check_package_files
from engine.plugins.lib.trivy_common.generate_composer_locks import check_composer_package_files
from engine.plugins.lib.utils import convert_string_to_json
from engine.plugins.lib.trivy_common.parsing_util import parse_output
from engine.plugins.lib.utils import setup_logging
Expand Down Expand Up @@ -36,11 +39,25 @@ def execute_trivy_lock_scan(path: str, include_dev: bool):
def main():
logger.info("Executing Trivy SCA")
args = parse_args()
path = abspath(args.path)
include_dev = args.engine_vars.get("include_dev", False)
results = []
alerts = []
errors = []

# Generate Lock files (and install npm packages for license info)
lock_file_errors, lock_file_alerts = check_package_files(args.path, include_dev, True)
alerts.extend(lock_file_alerts)
errors.extend(lock_file_errors)

# Run Composer Install for exact version numbers
(working_src, working_mount) = str(args.engine_vars.get("working_mount", "")).split(":")
if not working_src or not working_mount:
errors.append("Working volume not provided")

# Generate Lock files (without installing npm packages)
lock_file_errors, lock_file_alerts = check_package_files(args.path, include_dev, False)
compose_lock_errors, compose_lock_alerts = check_composer_package_files(path, working_src, include_dev)
alerts.extend(compose_lock_alerts)
errors.extend(compose_lock_errors)

# Scan local lock files
output = execute_trivy_lock_scan(args.path, include_dev)
Expand All @@ -54,11 +71,7 @@ def main():
results.extend(result)

# Return results
print(
json.dumps(
{"success": not bool(results), "details": results, "errors": lock_file_errors, "alerts": lock_file_alerts}
)
)
print(json.dumps({"success": not bool(results), "details": results, "errors": errors, "alerts": alerts}))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(minor) Since our JSON results can be quite large, we should switch from using json.dumps (which renders the whole JSON to a string first) to json.dump that renders the output directly to the output, e.g.:

json.dump({ ... }, sys.stdout)



if __name__ == "__main__":
Expand Down
1 change: 1 addition & 0 deletions backend/engine/plugins/trivy_sca/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"name": "Trivy SCA Scanner",
"type": "vulnerability",
"image": "$ECR/artemis/dind:latest",
"docker": true,
"build_images": false,
"enabled": true,
"writable": true
Expand Down
23 changes: 22 additions & 1 deletion backend/engine/tests/test_plugin_trivy_sca.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
from unittest.mock import patch
from oci import remover
from engine.plugins.trivy_sca import main as Trivy
from engine.plugins.lib.trivy_common.generate_locks import check_package_files
from engine.plugins.lib.trivy_common.generate_npm_locks import check_package_files
from engine.plugins.lib.trivy_common.generate_composer_locks import check_composer_package_files
from engine.plugins.lib.utils import convert_string_to_json
from engine.plugins.lib.utils import setup_logging

Expand Down Expand Up @@ -122,6 +123,16 @@ def test_lock_file_exists(self):
actual = check_package_files("/mocked/path/", False, False)
self.assertEqual(len(actual[1]), 0, "There should NOT be a warning of a lock file missing")

def test_compose_lock_file_exists(self):
with patch(f"{GENERATE_LOCKS_PREFIX}glob") as mock_glob:
mock_glob.return_value = ["/mocked/path/compose.json"]
with patch(f"{GENERATE_LOCKS_PREFIX}os.path.exists", return_value=True):
with patch(f"{GENERATE_LOCKS_PREFIX}subprocess.run") as mock_proc:
mock_proc.stderr = mock_proc.stdout = None
mock_proc.return_value = CompletedProcess(args="", returncode=0)
actual = check_composer_package_files("/mocked/path/", False)
self.assertEqual(len(actual[1]), 0, "There should NOT be a warning of a lock file missing")

def test_lock_file_missing(self):
with patch(f"{GENERATE_LOCKS_PREFIX}glob") as mock_glob:
mock_glob.return_value = ["/mocked/path/package.json"]
Expand All @@ -133,6 +144,16 @@ def test_lock_file_missing(self):
actual = check_package_files("/mocked/path/", False, False)
self.assertEqual(len(actual[1]), 1, "There should be a warning of a lock file missing")

def test_compose_lock_file_missing(self):
with patch(f"{GENERATE_LOCKS_PREFIX}glob") as mock_glob:
mock_glob.return_value = ["/mocked/path/compose.json"]
with patch(f"{GENERATE_LOCKS_PREFIX}os.path.exists", return_value=False):
with patch(f"{GENERATE_LOCKS_PREFIX}subprocess.run") as mock_proc:
mock_proc.stderr = mock_proc.stdout = None
mock_proc.return_value = CompletedProcess(args="", returncode=0)
actual = check_composer_package_files("/mocked/path/", False)
self.assertEqual(len(actual[1]), 1, "There should be a warning of a lock file missing")

def test_check_output(self):
check_output_list = Trivy.parse_output(self.demo_results_dict)
self.assertIn(TEST_CHECK_OUTPUT_PACKAGE_LOCK, check_output_list)
Expand Down
Loading