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)