diff --git a/README.md b/README.md index a7a889b..0635a6c 100644 --- a/README.md +++ b/README.md @@ -35,12 +35,15 @@ This Github Action is responsible for carrying out a dependency scan and produci The following options are available: -| Input Parameter | Description | Default Value | Required | -|----------------------|------------------------------------------------------|---------------------|----------| -| `json_filename` | The JSON output filename for the scan results | `dependencies.json` | false | -| `sarif_filename` | The SARIF output filename for the scan results | `results.sarif` | false | -| `scope` | The scope for the scan results | n/a | true | -| `publish_to_mend` | Whether to publish the scan results to Mend.io | `true` | true | +| Input Parameter | Description | Default Value | Required | +|----------------------|------------------------------------------------|-----------------------|----------| +| `json_filename` | The JSON output filename for the scan results | `dependencies.json` | false | +| `sarif_filename` | The SARIF output filename for the scan results | `results.sarif` | false | +| `scope` | The scope for the scan results | n/a | true | +| `github_url` | The URL for the GitHub repository | `https://github.com` | true | +| `github_repository` | The name of the GitHub repository | n/a | false | +| `publish_to_mend` | Whether to publish the scan results to Mend.io | `true` | true | +| `workflow_run` | The ID of the workflow run | n/a | true | ## The `scan-docker` Action diff --git a/scan-dependencies/action.yml b/scan-dependencies/action.yml index 0ed5b14..4c39def 100644 --- a/scan-dependencies/action.yml +++ b/scan-dependencies/action.yml @@ -2,6 +2,14 @@ name: 'Scan dependencies using the Mend.io CLI' description: 'Action to scan dependencies using the Mend.io CLI' inputs: + github_url: + description: 'The GitHub URL' + default: 'https://github.com' + required: true + repository: + description: 'The repository' + default: "${{ github.repository }}" + required: true json_filename: description: 'The JSON output filename for the scan results' default: 'dependencies.json' @@ -17,6 +25,9 @@ inputs: description: 'Whether to publish the scan results to Mend.io' default: 'true' required: true + workflow_run: + description: 'The workflow run to associate the scan results with' + required: true runs: using: 'composite' @@ -45,4 +56,9 @@ runs: - name: Run Python script shell: bash run: | - python ${GITHUB_ACTION_PATH}/mend-dependencies-sarif-converter.py + python ${GITHUB_ACTION_PATH}/mend-dependencies-sarif-converter.py \ + --input "${{ inputs.json_filename }}" \ + --output "${{ inputs.sarif_filename }}" \ + --github-url "${{ inputs.github_url }}" \ + --github-repository "${{ inputs.repository }}" \ + --workflow-run "${{ inputs.workflow_run }}" diff --git a/scan-dependencies/mend-dependencies-sarif-converter.py b/scan-dependencies/mend-dependencies-sarif-converter.py index e4e09c6..d50b14f 100644 --- a/scan-dependencies/mend-dependencies-sarif-converter.py +++ b/scan-dependencies/mend-dependencies-sarif-converter.py @@ -1,4 +1,7 @@ +import argparse import json +import os +from pathlib import Path def build_dependency_tree(dependency, indent="", is_last=True, current_vulnerability=None): """ @@ -9,8 +12,13 @@ def build_dependency_tree(dependency, indent="", is_last=True, current_vulnerabi current_line = f"{indent}{prefix}{dependency.get('name', 'unknown-artifact')}" vulnerabilities = dependency.get("vulnerabilities", []) + # Annotate all vulnerabilities if writing a full graph + if vulnerabilities and current_vulnerability is None: + vuln_names = ", ".join(v.get("name", "") for v in vulnerabilities) + current_line += f" ({vuln_names})" + # Annotate only the current vulnerability if specified - if current_vulnerability and any(v.get("name") == current_vulnerability for v in vulnerabilities): + elif current_vulnerability and any(v.get("name") == current_vulnerability for v in vulnerabilities): current_line += f" ({current_vulnerability})" # Recursively process children @@ -40,7 +48,31 @@ def traverse(dep): return vulnerable_dependencies -def create_sarif(vulnerable_dependencies, dependencies): +def detect_tool_type(dependency): + """ + Determine tool type (e.g., gradle, yarn) from dependency file. + """ + dep_file = dependency.get("dependencyFile", "").lower() + if "build.gradle" in dep_file or "build.gradle.kts" in dep_file: + return "gradle" + elif "yarn.lock" in dep_file: + return "yarn" + else: + return "unknown" + +def group_by_tool_type(dependencies): + """ + Group dependencies by their tool type. + """ + grouped = {} + for dep in dependencies: + tool_type = detect_tool_type(dep) + if tool_type not in grouped: + grouped[tool_type] = [] + grouped[tool_type].append(dep) + return grouped + +def create_sarif(vulnerable_dependencies, dependencies_by_tool): """ Create a SARIF object from the vulnerable dependencies. """ @@ -52,6 +84,9 @@ def create_sarif(vulnerable_dependencies, dependencies): for dep in vulnerable_dependencies: name = dep.get("name", "unknown-artifact") vulnerabilities = dep.get("vulnerabilities", []) + tool_type = detect_tool_type(dep) + tool_deps = dependencies_by_tool.get(tool_type, []) + for vuln in vulnerabilities: vuln_id = vuln.get("name", "unknown-vulnerability") @@ -87,21 +122,22 @@ def create_sarif(vulnerable_dependencies, dependencies): }) rule_ids.add(vuln_id) - # Build dependency tree for this specific vulnerability + # Build dependency tree for this specific vulnerability from correct tool group tree_for_sarif = "\n".join( - build_dependency_tree(dep, is_last=(i == len(dependencies) - 1), current_vulnerability=vuln_id) - for i, dep in enumerate(dependencies) + build_dependency_tree(root_dep, is_last=(i == len(tool_deps) - 1), current_vulnerability=vuln_id) + for i, root_dep in enumerate(tool_deps) ) + markdown_msg = f"Recommendations for [{vuln_id}]({url}):

" \ + f"* {fixResolution}.

" \ + f"[View dependency graphs]({github_url}/{github_repository}/actions/runs/{workflow_run})
" + # Add formatted details results.append({ "ruleId": vuln_id, "message": { "text": f"{title}", - "markdown": f"Recommendations for [{vuln_id}]({url}):

" - f"* {fixResolution}.

" - f"Dependency tree

" - f"{tree_for_sarif}
" + "markdown": markdown_msg }, "locations": [ { @@ -142,7 +178,25 @@ def create_sarif(vulnerable_dependencies, dependencies): } return sarif +def write_dependency_graphs(dependencies_by_tool): + """ + Write each dependency graph to a separate file based on tool type. + """ + output_dir = Path("dependency-graphs") + output_dir.mkdir(exist_ok=True) + + for tool_type, deps in dependencies_by_tool.items(): + tree = "\n".join( + build_dependency_tree(dep, is_last=(i == len(deps) - 1), current_vulnerability=None) + for i, dep in enumerate(deps) + ) + filename = output_dir / f"dependency-graph-{tool_type}.txt" + with open(filename, "w") as f: + f.write(tree) + print(f"📜 Dependency graph written to: {filename}") + def main(input_file, output_file): + """ Main script function. """ @@ -154,14 +208,20 @@ def main(input_file, output_file): if not isinstance(dependencies_data, list): raise ValueError("Unexpected JSON structure: Root must be a list.") - # Build the full Maven-style dependency tree - full_dependency_tree = "\n".join( - build_dependency_tree(dep, is_last=(i == len(dependencies_data) - 1)) - for i, dep in enumerate(dependencies_data) - ) + # Group dependencies by tool type (e.g., gradle, yarn) + dependencies_by_tool = group_by_tool_type(dependencies_data) - print("\nGenerated Full Dependency Tree:") - print(full_dependency_tree) + # Build and print full Maven-style dependency tree per tool type + print("\nGenerated Full Dependency Trees:") + for tool_type, deps in dependencies_by_tool.items(): + print(f"\nTool: {tool_type}") + print("\n".join( + build_dependency_tree(dep, is_last=(i == len(deps) - 1), current_vulnerability=None) + for i, dep in enumerate(deps) + )) + + # Write dependency graphs to files per tool + write_dependency_graphs(dependencies_by_tool) # Find vulnerable dependencies vulnerable_dependencies = find_vulnerable_dependencies(dependencies_data) @@ -170,7 +230,7 @@ def main(input_file, output_file): return # Create SARIF output - sarif_data = create_sarif(vulnerable_dependencies, dependencies_data) + sarif_data = create_sarif(vulnerable_dependencies, dependencies_by_tool) # Write SARIF to output file try: @@ -181,6 +241,20 @@ def main(input_file, output_file): print(f"Failed to write SARIF file: {e}") if __name__ == "__main__": - input_json = "dependencies.json" # Path to input JSON - output_sarif = "results.sarif" # Path to output SARIF file - main(input_json, output_sarif) + global github_url + global github_repository + global workflow_run + + parser = argparse.ArgumentParser(description="Convert dependencies to SARIF with optional GitHub workflow link.") + parser.add_argument("--github-url", help="The GitHub host URL") + parser.add_argument("--github-repository", help="The GitHub repository owner/name") + parser.add_argument("--input", default="dependencies.json", help="Path to input JSON file") + parser.add_argument("--output", default="results.sarif", help="Path to output SARIF file") + parser.add_argument("--workflow-run", help="GitHub Actions workflow run ID") + args = parser.parse_args() + + github_url = args.github_url + github_repository = args.github_repository + workflow_run = args.workflow_run + + main(args.input, args.output)