feat: add merge-sarif #37

Merged
t.behrendt merged 5 commits from feat-add-merge-sarif into main 2026-02-15 12:07:03 +01:00
5 changed files with 258 additions and 0 deletions
+28
View File
@@ -82,3 +82,31 @@ jobs:
echo "" echo ""
echo "All checks passed." 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: |
# 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."
+46
View File
@@ -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` |
+42
View File
@@ -0,0 +1,42 @@
name: "Merge SARIF Files"
description: "Merge multiple SARIF files into a single SARIF file"
author: "Timo Behrendt <t.behrendt@t00n.de>"
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 }}"
# Parse YAML list: lines like " - path/to/file" or "- file"
file_list=$(echo "$files" | sed -n 's/^[[:space:]]*-[[:space:]]*//p' | tr -d '"' | tr -d "'")
# 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"
+70
View File
@@ -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"
}
}
]
}
]
}
]
}
+72
View File
@@ -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"
}
}
]
}
]
}
]
}