diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..455dde1 --- /dev/null +++ b/.github/dependabot.yml @@ -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)" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c11fe9..6e45c2c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,22 +9,28 @@ 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' @@ -32,3 +38,236 @@ jobs: # `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; '-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-.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 diff --git a/.github/workflows/parity.yml b/.github/workflows/parity.yml index cc73b57..c49ca80 100644 --- a/.github/workflows/parity.yml +++ b/.github/workflows/parity.yml @@ -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 }} @@ -45,23 +46,23 @@ 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') }} @@ -69,7 +70,7 @@ jobs: ${{ 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 @@ -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 @@ -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: | diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2f2adbd..06abdb7 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -46,12 +46,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout (full history for git archive + release notes) - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 - name: Set up JDK 21 + GPG - uses: actions/setup-java@v4 + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 with: distribution: temurin java-version: '21' @@ -62,7 +62,7 @@ jobs: gpg-passphrase: MAVEN_GPG_PASSPHRASE - name: Set up Node.js (JS analyzer tests need a Node runtime) - uses: actions/setup-node@v4 + uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.4.0 with: node-version: '20' @@ -114,6 +114,56 @@ jobs: set -euo pipefail mvn -B -ntp -Prelease clean deploy + - name: Compute SHA256SUMS for release assets + # The bootstrap wrappers (scripts/sonar-cli.{sh,ps1}) verify the dist + # zip against a pinned SHA-256 before extracting. They read the expected + # hash from a sibling SHA256SUMS file at the same base URL, so emit one + # here. Entries use bare filenames (no path) so the wrappers can match + # by artifact name. Signing/attestation is a separate, out-of-scope item. + run: | + set -euo pipefail + VERSION="${{ steps.version.outputs.version }}" + SRC_ZIP="sonar-predict-${VERSION}-src.zip" + DIST_ZIP="target/sonar-predictor-dist-${VERSION}.zip" + if [ ! -f "${DIST_ZIP}" ]; then + echo "::error::distribution bundle not found at ${DIST_ZIP}" + ls -la target || true + exit 1 + fi + : > SHA256SUMS + ( cd "$(dirname "${DIST_ZIP}")" && sha256sum "$(basename "${DIST_ZIP}")" ) >> SHA256SUMS + if [ -f "${SRC_ZIP}" ]; then + sha256sum "${SRC_ZIP}" >> SHA256SUMS + fi + echo "SHA256SUMS:" + cat SHA256SUMS + + - name: GPG-sign release assets (detached, armored) + # Detached signatures over the release assets, using the SAME GPG key + # already imported by setup-java above (gpg-private-key / + # gpg-passphrase). Consumers can verify the dist zip and the checksum + # manifest against the project's public key. This is the GitHub-Release + # counterpart to the Central deploy's maven-gpg-plugin signing — both + # use the same key; neither replaces the other. + env: + MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }} + run: | + set -euo pipefail + VERSION="${{ steps.version.outputs.version }}" + SRC_ZIP="sonar-predict-${VERSION}-src.zip" + DIST_ZIP="target/sonar-predictor-dist-${VERSION}.zip" + sign() { + gpg --batch --yes --pinentry-mode loopback \ + --passphrase "${MAVEN_GPG_PASSPHRASE}" \ + --armor --detach-sign "$1" + echo "Signed $1 -> $1.asc" + } + sign SHA256SUMS + sign "${DIST_ZIP}" + if [ -f "${SRC_ZIP}" ]; then + sign "${SRC_ZIP}" + fi + - name: Create the GitHub Release env: GH_TOKEN: ${{ github.token }} @@ -132,8 +182,17 @@ jobs: else TAG="v${VERSION}" fi + ASSETS=( + "${SRC_ZIP}" + "${DIST_ZIP}" + "SHA256SUMS" + "SHA256SUMS.asc" + "${DIST_ZIP}.asc" + ) + if [ -f "${SRC_ZIP}.asc" ]; then + ASSETS+=("${SRC_ZIP}.asc") + fi gh release create "${TAG}" \ --title "sonar-predictor ${VERSION}" \ --generate-notes \ - "${SRC_ZIP}" \ - "${DIST_ZIP}" + "${ASSETS[@]}" diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index e521b74..42c3b88 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -35,14 +35,14 @@ jobs: # fetch-depth: 0 keeps the full history available so we can switch to # `--diff`-style semantics later without re-checking out the repo. - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 # JDK 21 is the project's build/runtime target (required by # sonarlint-analysis-engine 11.x / LTA 2026.1). Temurin is the safe default. - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 with: distribution: temurin java-version: '21' @@ -50,7 +50,7 @@ jobs: # The JS/TS analyzer plugin spawns Node at runtime to lint JS/TS sources, # so Node must be on PATH when the scan runs (not just at build time). - name: Set up Node.js 20 - uses: actions/setup-node@v4 + uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.4.0 with: node-version: '20' @@ -58,7 +58,7 @@ jobs: # dependency change invalidates cleanly; restore-keys lets a partial # cache hit still seed most of ~/.m2. - 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') }} @@ -146,7 +146,7 @@ jobs: # cover a typical PR review cycle without pinning storage forever. - name: Upload scan JSON if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: sonar-scan-${{ github.run_id }} path: .sonar-predictor/scan.json diff --git a/NOTICE b/NOTICE index bd90c07..9be5780 100644 --- a/NOTICE +++ b/NOTICE @@ -67,18 +67,20 @@ Build-time / direct-dependency Apache 2.0 components All Apache License 2.0. ------------------------------------------------------------------------------ -Runtime JRE auto-download (optional) +Java runtime ------------------------------------------------------------------------------ - When no Java 17+ runtime is found on the user's machine, the plugin's - bootstrap launcher fetches a JRE from the URL configured in - `plugin/skills/sonar-predictor/config.env`. The public default uses the - Adoptium Temurin API (https://api.adoptium.net), which serves Eclipse - Temurin OpenJDK builds under the GNU General Public License v2 with the - Classpath Exception (GPL-2.0 WITH Classpath-exception-2.0). + The shipped code provisions no JRE. The CLI and daemon run on the Java the + `bin/sonar` launcher discovers on the user's machine (`JAVA_HOME`, then + `PATH`, then common install locations); a Java 21+ runtime is required. - An air-gapped or corporate setup can replace this URL with a private JRE - mirror; the bootstrap does not require Adoptium specifically. + The OPTIONAL bootstrap wrappers `scripts/sonar-cli.sh` / `sonar-cli.ps1` + may, for an air-gapped or corporate install, fetch an Eclipse Temurin JDK + from a private Nexus base URL given by the `SONAR_NEXUS_BASE` environment + variable. Temurin OpenJDK builds are distributed under the GNU General + Public License v2 with the Classpath Exception (GPL-2.0 WITH + Classpath-exception-2.0). These wrappers are tooling, not part of the + installed runtime, and require no specific vendor. ================================================================================ NOTES ON THE BUNDLED MAVEN CENTRAL ARTIFACT @@ -95,5 +97,6 @@ individually from Maven Central using its published coordinates, so that SonarSource's own Maven Central distribution is the sole redistribution channel and `sonar-predict-dist-*.zip` no longer carries third-party JARs. This eliminates any redistribution question about the SSALv1-licensed -analyzers. The plugin's `config.env` already isolates the Maven repository -URL, so this transition is transparent to corporate Maven proxy setups. +analyzers. The `setup --repo ` option and the wrappers' `SONAR_NEXUS_BASE` +already isolate the repository base URL, so this transition is transparent to +corporate Maven proxy / Nexus setups. diff --git a/README.md b/README.md index 0ee2f35..7912641 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,34 @@ mvn -B clean package # output: target/sonar-predictor-dist-0.2.0-SNAPSHOT.zip ``` +### Enterprise / air-gapped install + +For corporate or air-gapped machines that cannot reach GitHub or Maven Central, +the bootstrap wrappers `scripts/sonar-cli.sh` (Linux/macOS) and +`scripts/sonar-cli.ps1` (Windows) fetch the distribution zip — and, when no +suitable Java is present, an Eclipse Temurin JDK — from an internal Nexus raw +repository instead. Point them at it with `SONAR_NEXUS_BASE` (the raw-repo base +URL, no trailing slash): + +```bash +export SONAR_NEXUS_BASE=https://nexus.example.com/repository/raw-hosted +./scripts/sonar-cli.sh check --diff +``` + +The wrappers verify artifact integrity before extracting anything: + +- `SONAR_DIST_SHA256` — expected lowercase-hex SHA-256 of the distribution zip. +- `SONAR_JDK_SHA256` — expected SHA-256 of the JDK archive. +- If those are unset, the wrapper fetches a `SHA256SUMS` sibling published next to + the artifact (one is published per release at + `{base}/sonar-predictor/{version}/SHA256SUMS`) and checks against that. +- `SONAR_ALLOW_UNVERIFIED=1` — proceed when *no* expected hash can be found. This + bypasses integrity checking and prints a loud warning; not recommended. + +With none of the above resolvable the wrapper refuses to extract unverified bytes. +You can also verify by hand before running — e.g. `sha256sum -c SHA256SUMS` — since +the wrappers use the same `sha256sum` (Linux) / `shasum -a 256` (macOS) tooling. + ## Usage ```bash @@ -67,6 +95,33 @@ Output formats: `--format sarif|json|text` (SARIF is the default). Add `--config (plus `--coverage-min N`) to fold in a JaCoCo / Cobertura / LCOV / Go / Clover / SimpleCov coverage report. +A few more options on `check` / `analyze`: + +- `--save ` — write the formatted report (per `--format`) to a file instead + of stdout. Stdout then carries a compact summary (issue count plus severity/type + rollup and the target file), so an agent or CI step gets a usable signal without + parsing the report or needing `jq`. +- `--test-path ` — treat files matching the glob as test code (repeatable, + additive). Augments the built-in test-path detection (`src/test/**`, `*Test.java`, + `*_test.go`, `*.spec.ts`, …) for non-standard layouts, e.g. + `--test-path 'src/integration/**'`. + +### Provisioning the analyzer runtime (`setup`) + +The distribution zip already bundles the analyzers, so `setup` is only needed when +you install from a thinner package or want to refresh `~/.sonar`: + +```bash +# Provision into ~/.sonar from Maven Central (default) +./bin/sonar setup + +# Pull the analyzer/engine JARs from a private Maven mirror / Nexus +./bin/sonar setup --repo https://nexus.example.com/repository/maven-public + +# Fully offline: provision from a local .tar.gz runtime bundle (no network) +./bin/sonar setup --offline /path/to/sonar-runtime.tar.gz +``` + ## Exit codes | Code | Meaning | @@ -107,6 +162,11 @@ The JSON output carries both fields on every issue. Nothing is downloaded at analysis time — the tool is fully offline once the zip is unpacked. +A Windows bootstrap wrapper (`scripts/sonar-cli.ps1`, alongside the +`scripts/sonar-cli.sh` POSIX one) ships for installing on Windows, but native +Windows analysis is not yet supported: the CLI ↔ daemon transport relies on a +Unix-domain (AF_UNIX) socket, so the daemon currently needs Linux, macOS, or WSL. + ## Scope `sonar-predictor` predicts the **rule-based** findings of a SonarQube server — bugs, diff --git a/pom.xml b/pom.xml index 1fa3a28..925b7fd 100644 --- a/pom.xml +++ b/pom.xml @@ -64,6 +64,16 @@ 21 21 UTF-8 + + 2026-01-01T00:00:00Z 2.17.2 5.10.2 @@ -192,6 +202,22 @@ maven-compiler-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + 900 + + + + + check + verify + check + + + + BUNDLE + + + LINE + COVEREDRATIO + 0.80 + + + + + + @@ -462,6 +514,46 @@ + + + sbom + + + + org.cyclonedx + cyclonedx-maven-plugin + 2.9.1 + + + make-bom + package + makeAggregateBom + + + + sonar-predictor-${project.version}-bom + + all + application + 1.5 + + + + + +