Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f8f3e80
feat: version-key the daemon socket to prevent stale-daemon adoption …
aksOps May 29, 2026
57431b0
feat(daemon): verify plugin jars against the manifest before loading …
aksOps May 29, 2026
473fd15
fix(cli): reject git-diff option injection; add XXE + tar guard tests…
aksOps May 29, 2026
9a38cd1
fix(setup): atomic offline-bundle extraction + owner-only execute cla…
aksOps May 29, 2026
b1e3614
fix(scripts): SHA-256-verify dist+JDK before extract; sign release as…
aksOps May 29, 2026
559e0b4
ci: CVE/SBOM gate, dependabot, test+job timeouts, pin actions to SHAs…
aksOps May 29, 2026
7532437
test: enforce cli to daemon boundary + manifest/jar drift guards (#20…
aksOps May 29, 2026
e0a941d
docs: reconcile README/NOTICE + javadocs with shipped behavior (#21, …
aksOps May 29, 2026
ab7b7c3
chore(ci): pin remaining cache/upload-artifact actions to commit SHAs…
aksOps May 29, 2026
f146e49
test(daemon): guard the embedded engine AnalyzeCommand API surface (#…
aksOps May 29, 2026
83c5bdb
feat(cli): add --timings round-trip measurement (stderr-only) + CI wa…
aksOps May 29, 2026
5873dde
build: byte-reproducible output (outputTimestamp) + reproducible/offl…
aksOps May 29, 2026
208bf22
build: enforce 0.80 line-coverage floor via JaCoCo check (was docs-on…
aksOps May 29, 2026
62e1360
fix(cli): report the real build version in --version, version cmd, an…
aksOps May 29, 2026
03d5463
fix(daemon): reset EngineLog per scan to stop unbounded growth across…
aksOps May 29, 2026
03be9a9
feat(cli): capture daemon child output to daemon.log and surface the …
aksOps May 29, 2026
2a875ac
perf(daemon): memoize the serialized rule catalog (output-identical) …
aksOps May 29, 2026
1eba0ae
refactor(daemon): consolidate repo->language mappings into one Langua…
aksOps May 29, 2026
d58d8b0
refactor: drop dead daemon/target probe, fix SARIF informationUri, ex…
aksOps May 29, 2026
de659a5
refactor(daemon): split analyzeLocked into focused steps; lift BaseDi…
aksOps May 29, 2026
29e1133
ci(fix): warm offline-build repo with a full verify so Surefire's JUn…
aksOps May 29, 2026
84c5540
ci(fix): CVE gate now feeds osv a recognized .cdx.json via -L and fai…
aksOps May 29, 2026
6206897
ci(fix): enforce HTTPS on osv-scanner download to clear SonarCloud ho…
aksOps May 29, 2026
18e11be
ci(fix): set least-privilege workflow permissions (contents: read) to…
aksOps May 29, 2026
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
36 changes: 36 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Dependabot keeps the Maven dependency tree and the GitHub Actions used in
# our workflows up to date. Weekly cadence keeps PR noise low for a project
# whose third-party surface (Jackson, picocli, SonarSource analyzers) moves
# slowly. Security updates from Dependabot complement the osv-scanner CVE
# gate in ci.yml: the gate blocks, Dependabot proposes the fix.
version: 2
updates:
- package-ecosystem: maven
directory: "/"
schedule:
interval: weekly
day: monday
open-pull-requests-limit: 5
labels:
- dependencies
commit-message:
prefix: "chore(deps)"
groups:
# Batch routine version bumps into one PR per ecosystem to cut churn;
# security updates still come through individually.
maven-minor-patch:
update-types:
- minor
- patch

- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
day: monday
open-pull-requests-limit: 5
labels:
- dependencies
- github-actions
commit-message:
prefix: "chore(ci)"
245 changes: 242 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,265 @@ on:
pull_request:
workflow_dispatch:

# Least privilege for GITHUB_TOKEN across every job (CodeQL S6504): these jobs
# only read the repo. The security job keeps its own explicit (identical) block.
permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Set up JDK 21
uses: actions/setup-java@v4
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: temurin
java-version: '21'
cache: maven

- name: Set up Node.js (JS/TS analyzer tests need a Node runtime)
uses: actions/setup-node@v4
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.4.0
with:
node-version: '20'

- name: Build and test
# `verify`, not `test`: the cli integration tests spawn the daemon and
# need its shaded fat jar, which is only built at the package phase.
run: mvn -B -ntp verify

# Dependency CVE gate + SBOM. Runs independently of `test` so neither blocks
# the other. The SBOM is produced via the `-Psbom` Maven profile (off by
# default — a plain `mvn verify` never touches it), then scanned offline with
# osv-scanner against a cached vulnerability DB. Gate policy (security.md):
# HIGH/CRITICAL (CVSS base score >= 7.0) hard-fail the build; Medium/Low are
# surfaced in the logs but do NOT block (documented, not gated).
security:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
env:
# osv-scanner reads its local DB from here; we cache this directory so
# the NVD/OSV feed is fetched at most once per week, never per-run, and
# the scan itself runs with --offline-vulnerabilities (no live feed).
OSV_SCANNER_LOCAL_DB_CACHE_DIRECTORY: ${{ github.workspace }}/.osv-db
OSV_SCANNER_VERSION: v2.3.8
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Set up JDK 21
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: temurin
java-version: '21'
cache: maven

- name: Generate CycloneDX SBOM
# `package`, because the SBOM aggregates the resolved dependency graph;
# -Psbom is the only thing that activates the cyclonedx plugin. Skip
# tests here — the `test` job already runs them; we only need the graph.
run: mvn -B -ntp -Psbom -DskipTests package
# Plugins/ jars are fetched as part of package; resolution stays inside
# Maven (settings.xml mirrors / Nexus / proxy apply), as elsewhere.

- name: Cache OSV vulnerability database
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: ${{ env.OSV_SCANNER_LOCAL_DB_CACHE_DIRECTORY }}
# Rotate weekly so the offline DB stays reasonably fresh without
# re-downloading on every run.
key: osv-db-${{ env.OSV_SCANNER_VERSION }}-${{ github.run_id }}
restore-keys: |
osv-db-${{ env.OSV_SCANNER_VERSION }}-

- name: Install osv-scanner (pinned)
run: |
set -euo pipefail
mkdir -p "$RUNNER_TEMP/osv"
# --proto '=https' --tlsv1.2: enforce HTTPS through redirects too, so a
# downgrade to http can never serve the binary (satisfies S6506).
curl --proto '=https' --tlsv1.2 -fsSL -o "$RUNNER_TEMP/osv/osv-scanner" \
"https://github.com/google/osv-scanner/releases/download/${OSV_SCANNER_VERSION}/osv-scanner_linux_amd64"
chmod +x "$RUNNER_TEMP/osv/osv-scanner"
echo "$RUNNER_TEMP/osv" >> "$GITHUB_PATH"

- name: Scan dependencies (offline DB) and gate on HIGH/CRITICAL
run: |
set -uo pipefail
mkdir -p "$OSV_SCANNER_LOCAL_DB_CACHE_DIRECTORY"
# outputName uses ${project.version}; resolve the actual emitted file.
SBOM=$(ls target/*-bom.json | head -n1)
if [ -z "${SBOM:-}" ] || [ ! -f "$SBOM" ]; then
echo "::error::CycloneDX SBOM not found under target/*-bom.json — cannot run the CVE gate."
exit 1
fi
# osv-scanner detects SBOM format from the FILENAME; '<name>-bom.json'
# is not a recognized CycloneDX name (it silently parses nothing), so
# copy to a recognized '.cdx.json' name before scanning.
SBOM_CDX="$RUNNER_TEMP/bom.cdx.json"
cp "$SBOM" "$SBOM_CDX"
echo "Scanning SBOM: $SBOM (as $SBOM_CDX)"
# --offline-vulnerabilities: never contacts the live feed during scan.
# --download-offline-databases: refresh the cached DB if cache missed.
# -L: lockfile/SBOM input (replaces the deprecated --sbom).
set +e
osv-scanner scan \
--offline-vulnerabilities \
--download-offline-databases \
--format json \
-L "$SBOM_CDX" > osv-results.json 2> osv-stderr.txt
OSV_EXIT=$?
set -e
cat osv-stderr.txt || true
# FAIL CLOSED: osv-scanner exits 0 (no vulns) or 1 (vulns found) on a
# real scan; any other exit code, a parse error, or a missing results
# object means NOTHING was scanned — the gate must fail, never pass on
# a non-scan (the bug this replaces let a non-scan pass green).
if [ "$OSV_EXIT" != "0" ] && [ "$OSV_EXIT" != "1" ]; then
echo "::error::osv-scanner did not complete a scan (exit $OSV_EXIT) — failing closed."
exit 1
fi
if grep -qiE "Failed to parse|invalid SBOM" osv-stderr.txt; then
echo "::error::osv-scanner could not ingest the SBOM — failing closed."
exit 1
fi
if ! jq -e 'has("results")' osv-results.json >/dev/null 2>&1; then
echo "::error::osv-scanner produced no results object — failing closed."
exit 1
fi
# Gate: max_severity is a CVSS base-score string per vulnerability
# group. >= 7.0 == HIGH or CRITICAL (CVSS v3). Anything below is
# non-blocking per security.md. An unscored group (null or "") is
# coerced to 0 so a missing CVSS never crashes or trips the gate.
SEV='((.max_severity // "0") | if . == "" then "0" else . end | tonumber)'
HIGH=$(jq "[.results[]?.packages[]?.groups[]? | ${SEV} | select(. >= 7.0)] | length" osv-results.json)
echo "HIGH/CRITICAL findings (CVSS >= 7.0): ${HIGH:-0}"
jq -r ".results[]?.packages[]? | .package as \$p | .groups[]? | ${SEV} as \$s | select(\$s >= 7.0) | \" BLOCK \(\$p.name)@\(\$p.version) CVSS=\(.max_severity) \(.ids | join(\",\"))\"" osv-results.json || true
if [ "${HIGH:-0}" -gt 0 ]; then
echo "::error::${HIGH} HIGH/CRITICAL dependency vulnerabilit(ies) found — failing per security policy."
exit 1
fi
echo "No HIGH/CRITICAL dependency vulnerabilities. (Medium/Low, if any, are non-blocking.)"

- name: Upload SBOM + scan results
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: sbom-and-cve-scan
path: |
target/*-bom.json
target/*-bom.xml
osv-results.json
if-no-files-found: warn

# Byte-reproducible build gate. Builds the dist artifact twice (clean
# between), with the fixed project.build.outputTimestamp from the pom
# normalizing every archive entry's timestamp and order. If the two
# sha256 digests of target/*dist*.zip differ, the build is not
# reproducible and the job fails. The dist zip is emitted by the
# maven-assembly-plugin `build-dist-zip` execution bound to the `package`
# phase (target/sonar-predictor-dist-<version>.zip), so `package` is the
# exact goal that produces it; -DskipTests still runs the
# process-test-classes shade execs the assembly's lib/ jars depend on.
reproducible:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Set up JDK 21
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: temurin
java-version: '21'
cache: maven

- name: Set up Node.js (JS/TS analyzer tests need a Node runtime)
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.4.0
with:
node-version: '20'

- name: Build dist (1st) and record digest
run: |
set -euo pipefail
mvn -B -ntp -DskipTests clean package
ZIP1=$(ls target/*dist*.zip | head -n1)
echo "First dist zip: $ZIP1"
sha256sum "$ZIP1" | awk '{print $1}' > /tmp/dist-hash-1.txt
cat /tmp/dist-hash-1.txt

- name: Build dist (2nd) and record digest
run: |
set -euo pipefail
mvn -B -ntp -DskipTests clean package
ZIP2=$(ls target/*dist*.zip | head -n1)
echo "Second dist zip: $ZIP2"
sha256sum "$ZIP2" | awk '{print $1}' > /tmp/dist-hash-2.txt
cat /tmp/dist-hash-2.txt

- name: Compare digests (fail if not byte-reproducible)
run: |
set -euo pipefail
H1=$(cat /tmp/dist-hash-1.txt)
H2=$(cat /tmp/dist-hash-2.txt)
echo "Build 1: $H1"
echo "Build 2: $H2"
if [ "$H1" != "$H2" ]; then
echo "::error::Dist zip is not byte-reproducible ($H1 != $H2)."
exit 1
fi
echo "Dist zip is byte-reproducible: $H1"

# Hermetic offline build. First warms the local repo (~/.m2) and the
# plugins/ analyzer copies over the network, then proves `mvn -o clean
# verify` succeeds with NO network access. dependency:go-offline alone is
# insufficient: the maven-dependency-plugin `copy` executions fetch the 10
# analyzer jars (and the host jar is built locally), and Surefire resolves
# its JUnit-platform provider only when tests actually run — so a full
# `verify` (tests included) is run first to populate ~/.m2 and plugins/
# before the offline verify.
offline-build:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Set up JDK 21
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: temurin
java-version: '21'
cache: maven

- name: Set up Node.js (JS/TS analyzer tests need a Node runtime)
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.4.0
with:
node-version: '20'

- name: Warm local repo (resolve plugins, deps, analyzer copies, test providers)
run: |
set -euo pipefail
# Resolve the plugin/dependency graph...
mvn -B -ntp dependency:go-offline
# ...then a FULL verify (tests included) to populate ~/.m2 with the
# analyzer jars pulled by the dependency:copy executions, the local
# host/shade jars the assembly needs, AND Surefire's lazily-resolved
# JUnit-platform provider. dependency:go-offline does NOT fetch that
# provider and -DskipTests never triggers it, so without running the
# tests here the offline verify below fails resolving
# surefire-junit-platform.
mvn -B -ntp verify

- name: Offline verify (no network)
run: |
set -euo pipefail
# -o forces fully offline: any unresolved artifact fails here,
# proving the warmed repo is self-sufficient.
mvn -B -o clean verify
27 changes: 21 additions & 6 deletions .github/workflows/parity.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ jobs:
parity:
name: Scan parity
runs-on: ubuntu-latest
timeout-minutes: 45
# Skip when the token isn't reachable (fork PRs) — the standalone
# self-scan workflow still gates fork PRs on our daemon's findings.
if: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository }}
Expand All @@ -45,31 +46,31 @@ jobs:
MAVEN_OPTS: -Xmx2g
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0

- name: Set up JDK 21
uses: actions/setup-java@v4
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: temurin
java-version: '21'

- name: Set up Node.js 20
uses: actions/setup-node@v4
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.4.0
with:
node-version: '20'

- name: Cache Maven repository
uses: actions/cache@v4
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-

- name: Cache SonarQube Cloud packages
uses: actions/cache@v4
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
Expand Down Expand Up @@ -126,6 +127,20 @@ jobs:
exit "$rc"
fi

# Informational only (non-gating): scan a fixture twice through the same
# (now-warm) daemon with --timings, echoing both analyze round-trip lines
# from stderr. Cold vs warm gives a rough sense of daemon reuse savings.
- name: Warm-scan benchmark (--timings, informational)
continue-on-error: true
run: |
set +e
export SONAR_PREDICTOR_HOME="$(pwd)/target/sonar-predictor-dist-${{ steps.version.outputs.version }}/sonar-predictor"
echo "--- first (cold) scan ---"
"$SONAR_PREDICTOR_HOME/bin/sonar" --timings --format json analyze . >/dev/null
echo "--- second (warm) scan ---"
"$SONAR_PREDICTOR_HOME/bin/sonar" --timings --format json analyze . >/dev/null
exit 0

# --- (B) SonarQube Cloud scan ------------------------------------------

- name: Run SonarQube Cloud scan
Expand Down Expand Up @@ -195,7 +210,7 @@ jobs:

- name: Upload scan + parity artifacts
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: scan-parity-${{ github.run_id }}
path: |
Expand Down
Loading
Loading