From 074556ca44e681a854081827178fced954afef5e Mon Sep 17 00:00:00 2001 From: Timo Behrendt Date: Fri, 13 Feb 2026 20:20:08 +0100 Subject: [PATCH 1/5] feat: add merge-sarif --- .gitea/workflows/ci.yaml | 22 +++++++++ merge-sarif-files/README.md | 46 +++++++++++++++++++ merge-sarif-files/action.yaml | 37 +++++++++++++++ merge-sarif-files/test_sarif1.json | 70 +++++++++++++++++++++++++++++ merge-sarif-files/test_sarif2.json | 72 ++++++++++++++++++++++++++++++ 5 files changed, 247 insertions(+) create mode 100644 merge-sarif-files/README.md create mode 100644 merge-sarif-files/action.yaml create mode 100644 merge-sarif-files/test_sarif1.json create mode 100644 merge-sarif-files/test_sarif2.json diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 3d34c12..f68a1d8 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -82,3 +82,25 @@ jobs: echo "" echo "All checks passed." + + test-merge-sarif-files: + name: Test Merge SARIF Files + runs-on: + - ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - name: Merge SARIF Files + uses: ./merge-sarif-files + with: + files: | + - merge-sarif-files/test_sarif1.json + - merge-sarif-files/test_sarif2.json + output-file: merged.sarif + - name: Check merged SARIF file + run: | + runs=$(jq -r '.runs' merged.sarif) + echo "Runs:" + echo "$runs" + # check that there are two runs in the array + [ "$(echo "$runs" | jq 'length')" -eq 2 ] && echo "OK" || { echo "FAIL (got $(echo "$runs" | jq 'length'), expected 2)"; exit 1; } + echo "All checks passed." diff --git a/merge-sarif-files/README.md b/merge-sarif-files/README.md new file mode 100644 index 0000000..4473099 --- /dev/null +++ b/merge-sarif-files/README.md @@ -0,0 +1,46 @@ +# Merge SARIF Files Action + +A reusable Gitea Action that merges multiple SARIF files into a single SARIF file. + +**Note:** This action only merges the files. It doesn't perform any validation, filtering, or deduplication. + +## Usage + +### Basic Usage + +```yaml +- name: Merge SARIF Files + uses: https://gitea.t000-n.de/t.behrendt/trivy-actions/merge-sarif-files@0.0.1 + with: + files: | + - test_sarif1.json + - test_sarif2.json + output-file: merged.sarif +``` + +### Complete Example + +```yaml +name: Merge SARIF Files +on: [push, pull_request] + +jobs: + merge-sarif-files: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Merge SARIF Files + uses: https://gitea.t000-n.de/t.behrendt/trivy-actions/merge-sarif-files@0.0.1 + with: + files: | + - test_sarif1.json + - test_sarif2.json + output-file: merged.sarif +``` + +## Inputs + +| Input | Description | Required | Default | +| ------------- | ----------------------------- | -------- | -------------- | +| `files` | Paths to SARIF files to merge | Yes | | +| `output-file` | Output file path | Yes | `merged.sarif` | diff --git a/merge-sarif-files/action.yaml b/merge-sarif-files/action.yaml new file mode 100644 index 0000000..089607e --- /dev/null +++ b/merge-sarif-files/action.yaml @@ -0,0 +1,37 @@ +name: "Merge SARIF Files" +description: "Merge multiple SARIF files into a single SARIF file" +author: "Timo Behrendt " +branding: + icon: "database" + color: "blue" + +inputs: + files: + description: "Paths to SARIF files to merge" + required: true + output-file: + description: "Output file path" + required: true + default: "merged.sarif" + +runs: + using: "composite" + steps: + - id: merge + shell: bash + run: | + set -e + files="${{ inputs.files }}" + output-file="${{ inputs.output-file }}" + + # Sarif files are easy to merge. They contain a "runs" array. We just need to concat the runs arrays and write the result to the output file. + # Collect all the runs from all + runs=() + for file in $files; do + runs+=($(jq -r '.runs' $file)) + done + + # Write the merged runs to the output file. + echo '{"version":"2.1.0","$schema":"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json","runs":[' > $output-file + echo "${runs[@]}" | jq -s '.' >> $output-file + echo ']}' >> $output-file diff --git a/merge-sarif-files/test_sarif1.json b/merge-sarif-files/test_sarif1.json new file mode 100644 index 0000000..17a3601 --- /dev/null +++ b/merge-sarif-files/test_sarif1.json @@ -0,0 +1,70 @@ +{ + "version": "2.1.0", + "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json", + "runs": [ + { + "tool": { + "driver": { + "fullName": "Trivy Vulnerability Scanner", + "informationUri": "https://github.com/aquasecurity/trivy", + "name": "Trivy", + "rules": [ + { + "id": "KSV-0014", + "name": "Misconfiguration", + "shortDescription": { + "text": "Root file system is not read-only" + }, + "fullDescription": { + "text": "An immutable root file system prevents applications from writing to their local disk. This can limit intrusions, as attackers will not be able to tamper with the file system or write foreign executables to disk." + }, + "defaultConfiguration": { + "level": "error" + }, + "helpUri": "https://avd.aquasec.com/misconfig/ksv-0014", + "help": { + "text": "Misconfiguration KSV-0014\nType: Kubernetes Security Check\nSeverity: HIGH\nCheck: Root file system is not read-only\nMessage: Container 'renovate' of CronJob 'renovate' should set 'securityContext.readOnlyRootFilesystem' to true\nLink: [KSV-0014](https://avd.aquasec.com/misconfig/ksv-0014)\nAn immutable root file system prevents applications from writing to their local disk. This can limit intrusions, as attackers will not be able to tamper with the file system or write foreign executables to disk.", + "markdown": "**Misconfiguration KSV-0014**\n| Type | Severity | Check | Message | Link |\n| --- | --- | --- | --- | --- |\n|Kubernetes Security Check|HIGH|Root file system is not read-only|Container 'renovate' of CronJob 'renovate' should set 'securityContext.readOnlyRootFilesystem' to true|[KSV-0014](https://avd.aquasec.com/misconfig/ksv-0014)|\n\nAn immutable root file system prevents applications from writing to their local disk. This can limit intrusions, as attackers will not be able to tamper with the file system or write foreign executables to disk." + }, + "properties": { + "precision": "very-high", + "security-severity": "8.0", + "tags": ["misconfiguration", "security", "HIGH"] + } + } + ], + "version": "0.69.1" + } + }, + "results": [ + { + "ruleId": "KSV-0014", + "ruleIndex": 0, + "level": "error", + "message": { + "text": "Artifact: k8s/31_deployment_redis.yaml\nType: kubernetes\nVulnerability KSV-0014\nSeverity: HIGH\nMessage: Container 'redis' of Deployment 'redis' should set 'securityContext.readOnlyRootFilesystem' to true\nLink: [KSV-0014](https://avd.aquasec.com/misconfig/ksv-0014)" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "k8s/31_deployment_redis.yaml", + "uriBaseId": "ROOTPATH" + }, + "region": { + "startLine": 34, + "startColumn": 1, + "endLine": 51, + "endColumn": 1 + } + }, + "message": { + "text": "k8s/31_deployment_redis.yaml" + } + } + ] + } + ] + } + ] +} diff --git a/merge-sarif-files/test_sarif2.json b/merge-sarif-files/test_sarif2.json new file mode 100644 index 0000000..5ae4188 --- /dev/null +++ b/merge-sarif-files/test_sarif2.json @@ -0,0 +1,72 @@ +{ + "version": "2.1.0", + "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json", + "runs": [ + { + "tool": { + "driver": { + "fullName": "Trivy Vulnerability Scanner", + "informationUri": "https://github.com/aquasecurity/trivy", + "name": "Trivy", + "rules": [ + { + "id": "CVE-2026-26019", + "name": "LanguageSpecificPackageVulnerability", + "shortDescription": { + "text": "@langchain/community: @langchain/community SSRF Bypass in RecursiveUrlLoader" + }, + "fullDescription": { + "text": "LangChain is a framework for building LLM-powered applications. Prior to 1.1.14, the RecursiveUrlLoader class in @langchain/community is a web crawler that recursively follows links from a starting URL. Its preventOutside option (enabled by default) is intended to restrict crawling to the same site as the base URL. The implementation used String.startsWith() to compare URLs, which does not perform semantic URL validation. An attacker who controls content on a crawled page could include links to domains that share a string prefix with the target, causing the crawler to follow links to attacker-controlled or internal infrastructure. Additionally, the crawler performed no validation against private or reserved IP addresses. A crawled page could include links targeting cloud metadata services, localhost, or RFC 1918 addresses, and the crawler would fetch them without restriction. This vulnerability is fixed in 1.1.14." + }, + "defaultConfiguration": { + "level": "warning" + }, + "helpUri": "https://avd.aquasec.com/nvd/cve-2026-26019", + "help": { + "text": "Vulnerability CVE-2026-26019\nSeverity: MEDIUM\nPackage: @langchain/community\nFixed Version: 1.1.14\nLink: [CVE-2026-26019](https://avd.aquasec.com/nvd/cve-2026-26019)\nLangChain is a framework for building LLM-powered applications. Prior to 1.1.14, the RecursiveUrlLoader class in @langchain/community is a web crawler that recursively follows links from a starting URL. Its preventOutside option (enabled by default) is intended to restrict crawling to the same site as the base URL. The implementation used String.startsWith() to compare URLs, which does not perform semantic URL validation. An attacker who controls content on a crawled page could include links to domains that share a string prefix with the target, causing the crawler to follow links to attacker-controlled or internal infrastructure. Additionally, the crawler performed no validation against private or reserved IP addresses. A crawled page could include links targeting cloud metadata services, localhost, or RFC 1918 addresses, and the crawler would fetch them without restriction. This vulnerability is fixed in 1.1.14.", + "markdown": "**Vulnerability CVE-2026-26019**\n| Severity | Package | Fixed Version | Link |\n| --- | --- | --- | --- |\n|MEDIUM|@langchain/community|1.1.14|[CVE-2026-26019](https://avd.aquasec.com/nvd/cve-2026-26019)|\n\nLangChain is a framework for building LLM-powered applications. Prior to 1.1.14, the RecursiveUrlLoader class in @langchain/community is a web crawler that recursively follows links from a starting URL. Its preventOutside option (enabled by default) is intended to restrict crawling to the same site as the base URL. The implementation used String.startsWith() to compare URLs, which does not perform semantic URL validation. An attacker who controls content on a crawled page could include links to domains that share a string prefix with the target, causing the crawler to follow links to attacker-controlled or internal infrastructure. Additionally, the crawler performed no validation against private or reserved IP addresses. A crawled page could include links targeting cloud metadata services, localhost, or RFC 1918 addresses, and the crawler would fetch them without restriction. This vulnerability is fixed in 1.1.14." + }, + "properties": { + "cvssv3_baseScore": 4.1, + "cvssv3_vector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:N/A:N", + "precision": "very-high", + "security-severity": "4.1", + "tags": ["vulnerability", "security", "MEDIUM"] + } + } + ], + "version": "0.69.1" + } + }, + "results": [ + { + "ruleId": "CVE-2026-26019", + "ruleIndex": 0, + "level": "warning", + "message": { + "text": "Package: @langchain/community\nInstalled Version: 0.3.36\nVulnerability CVE-2026-26019\nSeverity: MEDIUM\nFixed Version: 1.1.14\nLink: [CVE-2026-26019](https://avd.aquasec.com/nvd/cve-2026-26019)" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "bun.lock", + "uriBaseId": "ROOTPATH" + }, + "region": { + "startLine": 104, + "startColumn": 1, + "endLine": 104, + "endColumn": 1 + } + }, + "message": { + "text": "bun.lock: @langchain/community@0.3.36" + } + } + ] + } + ] + } + ] +} -- 2.52.0 From 78561ae2548a04ae76384ac78689830c13006f97 Mon Sep 17 00:00:00 2001 From: Timo Behrendt Date: Sun, 15 Feb 2026 11:40:15 +0100 Subject: [PATCH 2/5] fix action --- merge-sarif-files/action.yaml | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/merge-sarif-files/action.yaml b/merge-sarif-files/action.yaml index 089607e..648587a 100644 --- a/merge-sarif-files/action.yaml +++ b/merge-sarif-files/action.yaml @@ -24,14 +24,19 @@ runs: files="${{ inputs.files }}" output-file="${{ inputs.output-file }}" - # Sarif files are easy to merge. They contain a "runs" array. We just need to concat the runs arrays and write the result to the output file. - # Collect all the runs from all - runs=() - for file in $files; do - runs+=($(jq -r '.runs' $file)) - done + # Parse YAML list: lines like " - path/to/file" or "- file" + file_list=$(echo "$files" | sed -n 's/^[[:space:]]*-[[:space:]]*//p' | tr -d '"' | tr -d "'") - # Write the merged runs to the output file. - echo '{"version":"2.1.0","$schema":"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json","runs":[' > $output-file - echo "${runs[@]}" | jq -s '.' >> $output-file - echo ']}' >> $output-file + # Collect all runs from all SARIF files (each run as one compact JSON line) + runs_json=$(while IFS= read -r file; do + [ -z "$file" ] && continue + jq -c '.runs[]?' "$file" 2>/dev/null || true + done <<< "$file_list" | jq -s '.') + + # Take first file for version/schema, replace .runs with merged array + first_file=$(echo "$file_list" | head -1) + if [ -z "$first_file" ]; then + echo "No input files given." + exit 1 + fi + jq -n --argjson runs "$runs_json" --slurpfile first "$first_file" '$first[0] | .runs = $runs' > "$output-file" -- 2.52.0 From 3d3c36dc912f84b1ca076ba23484e5d24d5c4466 Mon Sep 17 00:00:00 2001 From: Timo Behrendt Date: Sun, 15 Feb 2026 11:43:13 +0100 Subject: [PATCH 3/5] fix ci --- .gitea/workflows/ci.yaml | 9 ++++----- merge-sarif-files/action.yaml | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index f68a1d8..4cac708 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -98,9 +98,8 @@ jobs: output-file: merged.sarif - name: Check merged SARIF file run: | - runs=$(jq -r '.runs' merged.sarif) - echo "Runs:" - echo "$runs" - # check that there are two runs in the array - [ "$(echo "$runs" | jq 'length')" -eq 2 ] && echo "OK" || { echo "FAIL (got $(echo "$runs" | jq 'length'), expected 2)"; exit 1; } + run_count=$(jq '.runs | length' merged.sarif) + [ "$run_count" -eq 2 ] || { echo "FAIL: expected 2 runs, got $run_count"; exit 1; } + echo "Merged SARIF has $run_count runs." + jq -e '.version and .["$schema"] and (.runs | length > 0)' merged.sarif >/dev/null || { echo "FAIL: invalid SARIF structure"; exit 1; } echo "All checks passed." diff --git a/merge-sarif-files/action.yaml b/merge-sarif-files/action.yaml index 648587a..550a0ca 100644 --- a/merge-sarif-files/action.yaml +++ b/merge-sarif-files/action.yaml @@ -22,7 +22,7 @@ runs: run: | set -e files="${{ inputs.files }}" - output-file="${{ inputs.output-file }}" + output_file="${{ inputs.output-file }}" # Parse YAML list: lines like " - path/to/file" or "- file" file_list=$(echo "$files" | sed -n 's/^[[:space:]]*-[[:space:]]*//p' | tr -d '"' | tr -d "'") @@ -39,4 +39,4 @@ runs: echo "No input files given." exit 1 fi - jq -n --argjson runs "$runs_json" --slurpfile first "$first_file" '$first[0] | .runs = $runs' > "$output-file" + jq -n --argjson runs "$runs_json" --slurpfile first "$first_file" '$first[0] | .runs = $runs' > "$output_file" -- 2.52.0 From 785aa30ee8d3f55039b64794d11a0d6876e280b4 Mon Sep 17 00:00:00 2001 From: Timo Behrendt Date: Sun, 15 Feb 2026 11:47:37 +0100 Subject: [PATCH 4/5] ci test for presence of ruleIds im nerged file --- .gitea/workflows/ci.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 4cac708..aa635ed 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -98,8 +98,15 @@ jobs: output-file: merged.sarif - name: Check merged SARIF file run: | + # Structure: version, schema, 2 runs run_count=$(jq '.runs | length' merged.sarif) [ "$run_count" -eq 2 ] || { echo "FAIL: expected 2 runs, got $run_count"; exit 1; } echo "Merged SARIF has $run_count runs." jq -e '.version and .["$schema"] and (.runs | length > 0)' merged.sarif >/dev/null || { echo "FAIL: invalid SARIF structure"; exit 1; } + + # Content: results from test_sarif1.json (ruleId KSV-0014) and test_sarif2.json (ruleId CVE-2026-26019) must be present + rule_ids=$(jq -r '[.runs[].results[].ruleId] | unique[]' merged.sarif) + echo "$rule_ids" | grep -q 'KSV-0014' || { echo "FAIL: merged file missing result from test_sarif1.json (ruleId KSV-0014)"; exit 1; } + echo "$rule_ids" | grep -q 'CVE-2026-26019' || { echo "FAIL: merged file missing result from test_sarif2.json (ruleId CVE-2026-26019)"; exit 1; } + echo "Merged results contain ruleIds from both input files." echo "All checks passed." -- 2.52.0 From 484cfd6c2275fededaffd91f5efe2e30a0b110ea Mon Sep 17 00:00:00 2001 From: Timo Behrendt Date: Sun, 15 Feb 2026 12:02:52 +0100 Subject: [PATCH 5/5] docs: fix indent --- merge-sarif-files/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/merge-sarif-files/README.md b/merge-sarif-files/README.md index 4473099..4ded285 100644 --- a/merge-sarif-files/README.md +++ b/merge-sarif-files/README.md @@ -35,7 +35,7 @@ jobs: files: | - test_sarif1.json - test_sarif2.json - output-file: merged.sarif + output-file: merged.sarif ``` ## Inputs -- 2.52.0