From 67360c429e1e2b639032fb006eda5d8d42b41ecf Mon Sep 17 00:00:00 2001 From: Thomas Cooper Date: Mon, 4 May 2026 00:35:43 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20secret=20scanning=20broken=20in=20Docker?= =?UTF-8?q?=20=E2=80=94=20missing=20git=20binary=20+=20exit=20code=20ambig?= =?UTF-8?q?uity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of #189: The Docker runtime image was missing the `git` binary. gitleaks git mode shells out to `git` to traverse commit history — without it, gitleaks exits 1 with an empty report. The proxy misread exit 1 as "findings present, read report", got an empty list, and silently passed. Secondary fix: use --exit-code 2 so gitleaks findings produce exit 2, making runtime errors (exit 1) unambiguous from clean scans (exit 0). Fail-closed: when secret-scan is enabled and the scanner errors or is unavailable, the push is now blocked with a clear message instead of silently passing. Smoke tests upgraded: fake truncated PEM keys replaced with real keys from openssl/ssh-keygen (with fallback detection). Added PKCS#8 (BEGIN PRIVATE KEY) alongside PKCS#1 (BEGIN RSA PRIVATE KEY). CI: docker-smoke-test job now runs as a matrix across db: [default, postgres, mongo] with fail-fast: false. Fixed compose.sh to prefer docker over podman when the docker daemon is confirmed running via `docker info`, avoiding a failure on GitHub Actions ubuntu-24.04 where the podman binary exists but its socket is not running. Descriptive error on blocked /info/refs: previously returned a bare 403. closes #189 closes #200 Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ci.yml | 63 +++++ Dockerfile | 4 +- compose.sh | 7 +- .../finos/gitproxy/git/GitleaksRunner.java | 19 +- .../gitproxy/git/SecretScanningHook.java | 24 +- .../servlet/filter/SecretScanningFilter.java | 8 +- .../filter/UrlRuleAggregateFilter.java | 9 +- .../gitproxy/git/GitleaksRunnerTest.java | 181 ++++++++++++++ .../filter/SecretScanningFilterTest.java | 10 +- .../servlet/filter/UrlRuleFilterTest.java | 12 +- .../frontend/package-lock.json | 224 ++++++++++-------- test/common.sh | 33 +++ test/gitea/env.sh | 20 ++ test/proxy-fail-secrets.sh | 22 +- test/push-fail-secrets.sh | 22 +- 15 files changed, 505 insertions(+), 153 deletions(-) create mode 100644 git-proxy-java-core/src/test/java/org/finos/gitproxy/git/GitleaksRunnerTest.java create mode 100644 test/gitea/env.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 194cbc49..04ba8d8f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,6 +53,69 @@ jobs: path: "**/build/reports/jacoco/" retention-days: 14 + docker-smoke-test: + name: CI / Docker Smoke Test (${{ matrix.db }}) + runs-on: ubuntu-latest + if: > + github.ref == 'refs/heads/main' || + github.event_name == 'pull_request' + strategy: + matrix: + db: [default, postgres, mongo] + # auth: [ldap, oidc] # Uncomment when auth smoke tests are added + fail-fast: false + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # ratchet:actions/checkout@v6 + + - name: Build and start stack + env: + DB: ${{ matrix.db }} + run: | + if [[ "$DB" != "default" ]]; then + bash compose.sh --db "$DB" -- up -d --build + else + bash compose.sh -- up -d --build + fi + timeout-minutes: 10 + + - name: Wait for proxy health + run: | + for i in $(seq 1 30); do + if curl -sf http://localhost:8080/api/health > /dev/null; then + echo "Proxy healthy after ${i}s" + break + fi + sleep 1 + done + curl -sf http://localhost:8080/api/health + + - name: Seed Gitea + run: bash docker/gitea-setup.sh + + - name: Run proxy smoke tests + run: | + source test/gitea/tokens.env + export GIT_PASSWORD="${GITEA_TESTUSER_TOKEN}" + export GIT_USERNAME="me" + export GIT_REPO="gitea/test-owner/test-repo.git" + export GIT_AUTHOR_NAME="test-user" + export GIT_EMAIL="testuser@example.com" + # Proxy mode only — store-and-forward passes queue for approval and hang + bash test/proxy-pass.sh + bash test/proxy-fail-secrets.sh + + - name: Dump proxy logs on failure + if: failure() + env: + DB: ${{ matrix.db }} + run: | + if [[ "$DB" != "default" ]]; then + bash compose.sh --db "$DB" -- logs git-proxy-java + else + bash compose.sh -- logs git-proxy-java + fi + dependency-submission: name: CI / Dependency Submission runs-on: ubuntu-latest diff --git a/Dockerfile b/Dockerfile index ad25f23b..9dd5282b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -# syntax=docker/dockerfile:1 +# syntax=docker/dockerfile:1@sha256:2780b5c3bab67f1f76c781860de469442999ed1a0d7992a5efdf2cffc0e3d769 # ── Build stage ────────────────────────────────────────────────────────────── FROM docker.io/eclipse-temurin:21-jdk@sha256:e58e492628c1428ceb838afc1a1b8762673d5eaa09296f560c363daea0fdcf3b AS builder @@ -51,6 +51,8 @@ FROM docker.io/eclipse-temurin:21-jre@sha256:ff65ff0d43c73d2b675eb4b758665a5cb48 WORKDIR /app +RUN apt-get update && apt-get install -y --no-install-recommends git && rm -rf /var/lib/apt/lists/* + # Copy the built distribution COPY --from=builder \ /workspace/git-proxy-java-dashboard/build/install/git-proxy-java-dashboard/ /app/ diff --git a/compose.sh b/compose.sh index 5a82d115..dede7c8e 100755 --- a/compose.sh +++ b/compose.sh @@ -48,10 +48,13 @@ if [[ -n "$DB" && "$DB" != "postgres" && "$DB" != "mongo" ]]; then exit 1 fi -if command -v podman &>/dev/null; then +if command -v docker &>/dev/null && docker info &>/dev/null 2>&1; then + COMPOSE="docker compose" +elif command -v podman &>/dev/null && podman info &>/dev/null 2>&1; then COMPOSE="podman compose" else - COMPOSE="docker compose" + echo "No working container runtime found (tried docker, podman)" >&2 + exit 1 fi ARGS=(-f docker-compose.yml) diff --git a/git-proxy-java-core/src/main/java/org/finos/gitproxy/git/GitleaksRunner.java b/git-proxy-java-core/src/main/java/org/finos/gitproxy/git/GitleaksRunner.java index bb1eef9e..b7e15221 100644 --- a/git-proxy-java-core/src/main/java/org/finos/gitproxy/git/GitleaksRunner.java +++ b/git-proxy-java-core/src/main/java/org/finos/gitproxy/git/GitleaksRunner.java @@ -40,8 +40,8 @@ *
  • System PATH - falls back to a {@code gitleaks} binary already installed on the host/container * * - *

    If no binary is found, scanning is skipped and an empty {@link Optional} is returned (fail-open). Pushes are never - * blocked due to scanner unavailability. + *

    If no binary is found or the scan errors, an empty {@link Optional} is returned. Whether the push is blocked is + * determined by the caller — callers with {@code secret-scan: enabled: true} block the push (fail-closed). * *

    The bundled binary extracted from the JAR is deleted on JVM shutdown. Binaries downloaded to {@code installDir} * persist across restarts. @@ -58,6 +58,9 @@ public class GitleaksRunner { /** Classpath resource name for the pre-bundled gitleaks binary (opt-in, not shipped by default). */ private static final String BUNDLED_BINARY_RESOURCE = "gitleaks"; + /** Exit code gitleaks returns when findings are present (distinct from error exit codes). */ + private static final int FINDINGS_EXIT_CODE = 2; + private static final String DEFAULT_INSTALL_DIR = System.getProperty("user.home") + "/.cache/git-proxy-java/gitleaks"; @@ -132,13 +135,13 @@ public Optional> scan(String diff, SecretScanConfig config) { if (exitCode == 0) { log.debug("gitleaks: no findings"); return Optional.of(Collections.emptyList()); - } else if (exitCode == 1) { + } else if (exitCode == FINDINGS_EXIT_CODE) { List findings = readFindings(reportFile); enrichFindings(findings, diff); log.debug("gitleaks: {} finding(s)", findings.size()); return Optional.of(findings); } else { - log.warn("gitleaks exited with code {} - secret scanning skipped (fail-open)", exitCode); + log.warn("gitleaks exited with code {} - treat as scanner error", exitCode); return Optional.empty(); } @@ -214,12 +217,12 @@ public Optional> scanGit(Path repoDir, String commitFrom, String c if (exitCode == 0) { log.debug("gitleaks git: no findings"); return Optional.of(Collections.emptyList()); - } else if (exitCode == 1) { + } else if (exitCode == FINDINGS_EXIT_CODE) { List findings = readFindings(reportFile); log.debug("gitleaks git: {} finding(s)", findings.size()); return Optional.of(findings); } else { - log.warn("gitleaks git exited with code {} - secret scanning skipped (fail-open)", exitCode); + log.warn("gitleaks git exited with code {} - treat as scanner error", exitCode); return Optional.empty(); } @@ -429,6 +432,8 @@ private static List buildCommand(Path binaryPath, Path reportFile, Path cmd.add("json"); cmd.add("--report-path"); cmd.add(reportFile.toString()); + cmd.add("--exit-code"); + cmd.add(String.valueOf(FINDINGS_EXIT_CODE)); if (configFilePath != null) { cmd.add("--config"); @@ -449,6 +454,8 @@ private static List buildGitCommand(Path binaryPath, String logOpts, Pat cmd.add(reportFile.toString()); cmd.add("--no-banner"); cmd.add("--redact"); + cmd.add("--exit-code"); + cmd.add(String.valueOf(FINDINGS_EXIT_CODE)); if (configFilePath != null) { cmd.add("--config"); diff --git a/git-proxy-java-core/src/main/java/org/finos/gitproxy/git/SecretScanningHook.java b/git-proxy-java-core/src/main/java/org/finos/gitproxy/git/SecretScanningHook.java index 9d37a46f..fed8ce79 100644 --- a/git-proxy-java-core/src/main/java/org/finos/gitproxy/git/SecretScanningHook.java +++ b/git-proxy-java-core/src/main/java/org/finos/gitproxy/git/SecretScanningHook.java @@ -25,7 +25,8 @@ * to limit scanning to commits not already reachable from any existing ref; branch updates use * {@code commitFrom..commitTo}. * - *

    If the scanner is unavailable the hook continues (fail-open), recording a SKIPPED step. + *

    If the scanner is unavailable and secret scanning is enabled, the push is blocked (fail-closed). If scanning is + * disabled the hook records a SKIPPED step. */ @Slf4j @RequiredArgsConstructor @@ -61,7 +62,6 @@ public void onPreReceive(ReceivePack rp, Collection commands) { Optional> result = runner.scanGit(repoDir, commitFrom, commitTo, config); if (result.isEmpty()) { - // Fail-open: scanner unavailable or errored - GitleaksRunner already logged the detail scannerUnavailable = true; continue; } @@ -72,13 +72,19 @@ public void onPreReceive(ReceivePack rp, Collection commands) { } } - if (scannerUnavailable && allViolations.isEmpty()) { - pushContext.addStep(PushStep.builder() - .stepName(STEP_NAME) - .stepOrder(ORDER) - .status(StepStatus.SKIPPED) - .build()); - return; + if (scannerUnavailable) { + if (config.isEnabled()) { + String msg = "Secret scanning failed — scanner error or unavailable. " + + "Push blocked because secret-scan is enabled. Check server logs for details."; + allViolations.add(new Violation(msg, msg, sym(CROSS_MARK) + " " + msg)); + } else { + pushContext.addStep(PushStep.builder() + .stepName(STEP_NAME) + .stepOrder(ORDER) + .status(StepStatus.SKIPPED) + .build()); + return; + } } if (allViolations.isEmpty()) { diff --git a/git-proxy-java-core/src/main/java/org/finos/gitproxy/servlet/filter/SecretScanningFilter.java b/git-proxy-java-core/src/main/java/org/finos/gitproxy/servlet/filter/SecretScanningFilter.java index 064df0cc..d01fabef 100644 --- a/git-proxy-java-core/src/main/java/org/finos/gitproxy/servlet/filter/SecretScanningFilter.java +++ b/git-proxy-java-core/src/main/java/org/finos/gitproxy/servlet/filter/SecretScanningFilter.java @@ -25,6 +25,9 @@ * correctly because gitleaks has full file-path context when operating in git mode. * *

    Runs at order 340, after {@link ScanDiffFilter} (order 300), within the content filters range (200-399). + * + *

    If the scanner errors or is unavailable when scanning is enabled, the push is blocked (fail-closed) with a clear + * error message. Check server logs for the underlying cause. */ @Slf4j public class SecretScanningFilter extends AbstractGitProxyFilter { @@ -95,7 +98,10 @@ public void doHttpFilter(HttpServletRequest request, HttpServletResponse respons runner.scanGit(repo.getDirectory().toPath(), commitFrom, commitTo, config); if (result.isEmpty()) { - // Fail-open: scanner unavailable - GitleaksRunner already logged the detail + String msg = "Secret scanning failed — scanner error or unavailable. " + + "Push blocked because secret-scan is enabled. Check server logs for details."; + log.error("Secret scanner returned no result — blocking push (fail-closed)"); + recordIssue(request, msg, sym(CROSS_MARK) + " " + msg); return; } diff --git a/git-proxy-java-core/src/main/java/org/finos/gitproxy/servlet/filter/UrlRuleAggregateFilter.java b/git-proxy-java-core/src/main/java/org/finos/gitproxy/servlet/filter/UrlRuleAggregateFilter.java index ba81f8c0..c3202f42 100644 --- a/git-proxy-java-core/src/main/java/org/finos/gitproxy/servlet/filter/UrlRuleAggregateFilter.java +++ b/git-proxy-java-core/src/main/java/org/finos/gitproxy/servlet/filter/UrlRuleAggregateFilter.java @@ -162,13 +162,18 @@ private void applyInfoRefsRules(HttpServletRequest request, HttpServletResponse log.debug("Blocking /info/refs — matched deny rule: {}", d.ruleId()); if (effectiveOp == HttpOperation.FETCH && fetchStore != null) recordFetch(request, false); setResult(request, GitRequestDetails.GitResult.REJECTED, "Repository blocked by deny rule"); - response.sendError(provider.getBlockedInfoRefsStatus()); + response.sendError( + provider.getBlockedInfoRefsStatus(), + "Repository access denied: this repository has been explicitly blocked by an administrator."); } case UrlRuleEvaluator.Result.NotAllowed n -> { log.debug("Blocking /info/refs — no rule matched"); if (effectiveOp == HttpOperation.FETCH && fetchStore != null) recordFetch(request, false); setResult(request, GitRequestDetails.GitResult.REJECTED, "Repository not in allow rules"); - response.sendError(provider.getBlockedInfoRefsStatus()); + response.sendError( + provider.getBlockedInfoRefsStatus(), + "Repository access denied: this repository is not in the allow list." + + " Contact an administrator to add it."); } case UrlRuleEvaluator.Result.Allowed a -> { /* pass through — request is permitted */ diff --git a/git-proxy-java-core/src/test/java/org/finos/gitproxy/git/GitleaksRunnerTest.java b/git-proxy-java-core/src/test/java/org/finos/gitproxy/git/GitleaksRunnerTest.java new file mode 100644 index 00000000..e7bc4126 --- /dev/null +++ b/git-proxy-java-core/src/test/java/org/finos/gitproxy/git/GitleaksRunnerTest.java @@ -0,0 +1,181 @@ +package org.finos.gitproxy.git; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.*; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.revwalk.RevCommit; +import org.finos.gitproxy.config.SecretScanConfig; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Tests that GitleaksRunner detects RSA/PKCS#8 private keys via both scanning modes: - scanGit(): git-native mode used + * by SecretScanningHook and SecretScanningFilter - scan(): --pipe diff mode used by SecretScanCheck + * + *

    These tests run the real gitleaks binary bundled in the JAR. They are skipped if the binary is unavailable (CI + * environments without the bundled resource). + */ +class GitleaksRunnerTest { + + // Structurally valid PKCS#8 PEM block with fake base64 content. + // The gitleaks private-key rule matches on the header line, not the key material. + private static final String FAKE_PKCS8_KEY = """ + -----BEGIN PRIVATE KEY----- + MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCWv7Dvs6PjZJZ0 + Lh6xvKGwuqCoULGNd75VwkNBLFTEM7ME3jEjPPej3td5BayTIBzRUYnjpU7J1qO0 + zkSUDDrhFpPEoJMDyF2Ml1d1r9EjF52tKc0qzmHeJTCbmLmJmEhbNDrwcX5NbCo2 + Lv9yNVn4qBMp7mfJEh8i3DSW4mhYuFATnUxEw5KxXyx/t53V52qa2euodWjRl4Llt + eFCZHfqznLo1mi5R5fINwlx1UspD0ItPmQ2eXc0QfUsgTQwj3b1B5VgFzjcBngThI + BknQrajJHzL60QaSkSlkUVEr7+yE2MIMLtD6kIZR58t0yhd81xY7pwETZ6dOykeXP + X0C/AgMBAAEC + -----END PRIVATE KEY----- + """; + + // Same key wrapped in a unified diff (as produced by git diff / git format-patch). + // Each content line is prefixed with '+' — this is what GitleaksRunner.scan() receives. + private static final String FAKE_KEY_IN_DIFF = """ + diff --git a/private.key b/private.key + new file mode 100644 + index 0000000..1234567 + --- /dev/null + +++ b/private.key + @@ -0,0 +1,12 @@ + +-----BEGIN PRIVATE KEY----- + +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCWv7Dvs6PjZJZ0 + +Lh6xvKGwuqCoULGNd75VwkNBLFTEM7ME3jEjPPej3td5BayTIBzRUYnjpU7J1qO0 + +zkSUDDrhFpPEoJMDyF2Ml1d1r9EjF52tKc0qzmHeJTCbmLmJmEhbNDrwcX5NbCo2 + +Lv9yNVn4qBMp7mfJEh8i3DSW4mhYuFATnUxEw5KxXyx/t53V52qa2euodWjRl4Llt + +eFCZHfqznLo1mi5R5fINwlx1UspD0ItPmQ2eXc0QfUsgTQwj3b1B5VgFzjcBngThI + +BknQrajJHzL60QaSkSlkUVEr7+yE2MIMLtD6kIZR58t0yhd81xY7pwETZ6dOykeXP + +X0C/AgMBAAEC + +-----END PRIVATE KEY----- + """; + + @TempDir + Path tempDir; + + private SecretScanConfig enabledConfig() { + return SecretScanConfig.builder().enabled(true).build(); + } + + /** Skips the test if the gitleaks binary cannot be resolved (bundled resource absent). */ + private GitleaksRunner runnerOrSkip() { + GitleaksRunner runner = new GitleaksRunner(); + // Probe: if resolveBinaryPath returns null, the binary is unavailable — skip rather than fail. + Optional> probe = runner.scan("probe text", enabledConfig()); + assumeTrue(probe.isPresent(), "gitleaks binary not available — skipping"); + return runner; + } + + // ── scanGit() — git-native mode ────────────────────────────────────────── + + @Test + void scanGit_detectsPrivateKeyInCommit(@TempDir Path repoDir) throws Exception { + GitleaksRunner runner = runnerOrSkip(); + + Git git = Git.init().setDirectory(repoDir.toFile()).call(); + git.getRepository().getConfig().setBoolean("commit", null, "gpgsign", false); + git.getRepository().getConfig().save(); + + // Base commit — already merged into the branch before the "push" arrives + Files.writeString(repoDir.resolve("readme.txt"), "Hello world"); + git.add().addFilepattern("readme.txt").call(); + RevCommit base = git.commit() + .setAuthor(new PersonIdent("Test", "test@example.com")) + .setCommitter(new PersonIdent("Test", "test@example.com")) + .setMessage("initial commit") + .call(); + + // Bad commit — simulates the new commit being pushed (base..bad is the scan range) + File keyFile = repoDir.resolve("private.key").toFile(); + Files.writeString(keyFile.toPath(), FAKE_PKCS8_KEY); + git.add().addFilepattern("private.key").call(); + RevCommit bad = git.commit() + .setAuthor(new PersonIdent("Test", "test@example.com")) + .setCommitter(new PersonIdent("Test", "test@example.com")) + .setMessage("add private key") + .call(); + + // Use commitFrom..commitTo (branch-update range) — mirrors what SecretScanningHook does + Optional> result = + runner.scanGit(repoDir, base.name(), bad.name(), enabledConfig()); + + assertTrue(result.isPresent(), "Scanner must return a result (not fail-open)"); + assertFalse(result.get().isEmpty(), "gitleaks must detect the PKCS#8 private key in git-native mode"); + assertTrue( + result.get().stream().anyMatch(f -> "private-key".equals(f.getRuleId())), + "Finding must match the 'private-key' rule"); + } + + @Test + void scanGit_cleanCommit_noFindings(@TempDir Path repoDir) throws Exception { + GitleaksRunner runner = runnerOrSkip(); + + Git git = Git.init().setDirectory(repoDir.toFile()).call(); + git.getRepository().getConfig().setBoolean("commit", null, "gpgsign", false); + git.getRepository().getConfig().save(); + + Files.writeString(repoDir.resolve("readme.txt"), "Hello"); + git.add().addFilepattern("readme.txt").call(); + RevCommit base = git.commit() + .setAuthor(new PersonIdent("Test", "test@example.com")) + .setCommitter(new PersonIdent("Test", "test@example.com")) + .setMessage("initial") + .call(); + + Files.writeString(repoDir.resolve("feature.txt"), "just code, no secrets"); + git.add().addFilepattern("feature.txt").call(); + RevCommit clean = git.commit() + .setAuthor(new PersonIdent("Test", "test@example.com")) + .setCommitter(new PersonIdent("Test", "test@example.com")) + .setMessage("clean commit") + .call(); + + Optional> result = + runner.scanGit(repoDir, base.name(), clean.name(), enabledConfig()); + + assertTrue(result.isPresent(), "Scanner must return a result"); + assertTrue(result.get().isEmpty(), "Clean commit must produce no findings"); + } + + // ── scan() — --pipe diff mode ───────────────────────────────────────────── + + @Test + void scan_pipeMode_detectsPrivateKeyInDiff() { + GitleaksRunner runner = runnerOrSkip(); + + Optional> result = runner.scan(FAKE_KEY_IN_DIFF, enabledConfig()); + + assertTrue(result.isPresent(), "Scanner must return a result (not fail-open)"); + assertFalse( + result.get().isEmpty(), + "gitleaks --pipe mode must detect the PKCS#8 private key embedded in a unified diff. " + + "If this assertion fails, the '+' diff prefix is preventing the private-key regex from matching."); + } + + @Test + void scan_pipeMode_cleanDiff_noFindings() { + GitleaksRunner runner = runnerOrSkip(); + + String cleanDiff = """ + diff --git a/readme.txt b/readme.txt + new file mode 100644 + --- /dev/null + +++ b/readme.txt + @@ -0,0 +1,1 @@ + +Hello world + """; + + Optional> result = runner.scan(cleanDiff, enabledConfig()); + + assertTrue(result.isPresent(), "Scanner must return a result"); + assertTrue(result.get().isEmpty(), "Clean diff must produce no findings"); + } +} diff --git a/git-proxy-java-core/src/test/java/org/finos/gitproxy/servlet/filter/SecretScanningFilterTest.java b/git-proxy-java-core/src/test/java/org/finos/gitproxy/servlet/filter/SecretScanningFilterTest.java index d91b4a81..2c01a636 100644 --- a/git-proxy-java-core/src/test/java/org/finos/gitproxy/servlet/filter/SecretScanningFilterTest.java +++ b/git-proxy-java-core/src/test/java/org/finos/gitproxy/servlet/filter/SecretScanningFilterTest.java @@ -11,6 +11,7 @@ import java.util.Optional; import org.eclipse.jgit.lib.Repository; import org.finos.gitproxy.config.SecretScanConfig; +import org.finos.gitproxy.db.model.StepStatus; import org.finos.gitproxy.git.GitRequestDetails; import org.finos.gitproxy.git.GitleaksRunner; import org.finos.gitproxy.git.HttpOperation; @@ -117,14 +118,17 @@ void blankCommitTo_skipped() throws Exception { // ---- scanner results ---- @Test - void scannerUnavailable_failOpen() throws Exception { + void scannerUnavailable_failClosed() throws Exception { GitRequestDetails details = pushDetailsWithRepo(); when(runner.scanGit(any(), any(), any(), any())).thenReturn(Optional.empty()); filter.doHttpFilter(requestWith(details), mock(HttpServletResponse.class)); - // fail-open: no steps added when scanner is unavailable - assertTrue(details.getSteps().isEmpty()); + // fail-closed: scanner error with enabled=true must block the push + assertFalse(details.getSteps().isEmpty()); + assertTrue( + details.getSteps().stream().anyMatch(s -> s.getStatus() == StepStatus.FAIL), + "Scanner error must produce a FAIL step when secret-scan is enabled"); } @Test diff --git a/git-proxy-java-core/src/test/java/org/finos/gitproxy/servlet/filter/UrlRuleFilterTest.java b/git-proxy-java-core/src/test/java/org/finos/gitproxy/servlet/filter/UrlRuleFilterTest.java index 4e16f579..46037432 100644 --- a/git-proxy-java-core/src/test/java/org/finos/gitproxy/servlet/filter/UrlRuleFilterTest.java +++ b/git-proxy-java-core/src/test/java/org/finos/gitproxy/servlet/filter/UrlRuleFilterTest.java @@ -281,7 +281,7 @@ void infoRefs_noAllowRule_returns403() throws Exception { aggregate.doHttpFilter(mockInfoRefsRequest(details, "git-upload-pack"), resp.mock); - verify(resp.mock).sendError(403); + verify(resp.mock).sendError(eq(403), anyString()); assertEquals(GitRequestDetails.GitResult.REJECTED, details.getResult()); } @@ -345,7 +345,7 @@ void infoRefs_receivePack_deniedByRule_returns403() throws Exception { aggregate.doHttpFilter(mockInfoRefsRequest(details, "git-receive-pack"), resp.mock); - verify(resp.mock).sendError(403); + verify(resp.mock).sendError(eq(403), anyString()); } // --- Gap 2: recordFetch on blocked /info/refs --- @@ -360,7 +360,7 @@ void infoRefs_fetchBlocked_notAllowed_recordsFetch() throws Exception { aggregate.doHttpFilter(mockInfoRefsRequest(details, "git-upload-pack"), resp.mock); - verify(resp.mock).sendError(403); + verify(resp.mock).sendError(eq(403), anyString()); ArgumentCaptor captor = ArgumentCaptor.forClass(FetchRecord.class); verify(fetchStore).record(captor.capture()); assertEquals(FetchRecord.Result.BLOCKED, captor.getValue().getResult()); @@ -382,7 +382,7 @@ void infoRefs_fetchBlocked_denyRule_recordsFetch() throws Exception { aggregate.doHttpFilter(mockInfoRefsRequest(details, "git-upload-pack"), resp.mock); - verify(resp.mock).sendError(403); + verify(resp.mock).sendError(eq(403), anyString()); ArgumentCaptor captor = ArgumentCaptor.forClass(FetchRecord.class); verify(fetchStore).record(captor.capture()); assertEquals(FetchRecord.Result.BLOCKED, captor.getValue().getResult()); @@ -398,7 +398,7 @@ void infoRefs_pushBlocked_doesNotRecordFetch() throws Exception { aggregate.doHttpFilter(mockInfoRefsRequest(details, "git-receive-pack"), resp.mock); - verify(resp.mock).sendError(403); + verify(resp.mock).sendError(eq(403), anyString()); verify(fetchStore, never()).record(any()); } @@ -437,6 +437,6 @@ void infoRefs_customBlockedStatus_returns404() throws Exception { aggregate.doHttpFilter(mockInfoRefsRequest(details, "git-upload-pack"), resp.mock); - verify(resp.mock).sendError(404); + verify(resp.mock).sendError(eq(404), anyString()); } } diff --git a/git-proxy-java-dashboard/frontend/package-lock.json b/git-proxy-java-dashboard/frontend/package-lock.json index 5143f6e9..afe3a147 100644 --- a/git-proxy-java-dashboard/frontend/package-lock.json +++ b/git-proxy-java-dashboard/frontend/package-lock.json @@ -272,9 +272,9 @@ } }, "node_modules/@emnapi/core": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", - "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", "dev": true, "license": "MIT", "optional": true, @@ -284,9 +284,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", - "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "dev": true, "license": "MIT", "optional": true, @@ -565,9 +565,9 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz", - "integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", "dev": true, "license": "MIT", "optional": true, @@ -584,9 +584,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.124.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz", - "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==", + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", + "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", "dev": true, "license": "MIT", "funding": { @@ -606,9 +606,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz", - "integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==", "cpu": [ "arm64" ], @@ -623,9 +623,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz", - "integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", "cpu": [ "arm64" ], @@ -640,9 +640,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz", - "integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", "cpu": [ "x64" ], @@ -657,9 +657,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz", - "integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", "cpu": [ "x64" ], @@ -674,9 +674,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz", - "integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", + "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", "cpu": [ "arm" ], @@ -691,13 +691,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz", - "integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -708,13 +711,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz", - "integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -725,13 +731,16 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz", - "integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -742,13 +751,16 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz", - "integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -759,13 +771,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz", - "integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -776,13 +791,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz", - "integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -793,9 +811,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz", - "integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==", "cpu": [ "arm64" ], @@ -810,9 +828,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz", - "integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", + "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", "cpu": [ "wasm32" ], @@ -820,18 +838,18 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "1.9.2", - "@emnapi/runtime": "1.9.2", - "@napi-rs/wasm-runtime": "^1.1.3" + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" }, "engines": { - "node": ">=14.0.0" + "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz", - "integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", "cpu": [ "arm64" ], @@ -846,9 +864,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz", - "integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", "cpu": [ "x64" ], @@ -2885,9 +2903,9 @@ } }, "node_modules/postcss": { - "version": "8.5.9", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", - "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz", + "integrity": "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==", "dev": true, "funding": [ { @@ -2924,9 +2942,9 @@ } }, "node_modules/prettier": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", - "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", "dev": true, "license": "MIT", "bin": { @@ -2971,9 +2989,9 @@ } }, "node_modules/react-router": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.0.tgz", - "integrity": "sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ==", + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.2.tgz", + "integrity": "sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -2993,12 +3011,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.0.tgz", - "integrity": "sha512-2G3ajSVSZMEtmTjIklRWlNvo8wICEpLihfD/0YMDxbWK2UyP5EGfnoIn9AIQGnF3G/FX0MRbHXdFcD+rL1ZreQ==", + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.2.tgz", + "integrity": "sha512-YZcM5ES8jJSM+KrJ9BdvHHqlnGTg5tH3sC5ChFRj4inosKctdyzBDhOyyHdGk597q2OT6NTrCA1OvB/YDwfekQ==", "license": "MIT", "dependencies": { - "react-router": "7.14.0" + "react-router": "7.14.2" }, "engines": { "node": ">=20.0.0" @@ -3019,14 +3037,14 @@ } }, "node_modules/rolldown": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz", - "integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", + "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.124.0", - "@rolldown/pluginutils": "1.0.0-rc.15" + "@oxc-project/types": "=0.127.0", + "@rolldown/pluginutils": "1.0.0-rc.17" }, "bin": { "rolldown": "bin/cli.mjs" @@ -3035,27 +3053,27 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.15", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", - "@rolldown/binding-darwin-x64": "1.0.0-rc.15", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" + "@rolldown/binding-android-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-x64": "1.0.0-rc.17", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" } }, "node_modules/rolldown/node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", - "integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", + "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", "dev": true, "license": "MIT" }, @@ -3299,17 +3317,17 @@ } }, "node_modules/vite": { - "version": "8.0.8", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz", - "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", + "version": "8.0.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", + "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", - "postcss": "^8.5.8", - "rolldown": "1.0.0-rc.15", - "tinyglobby": "^0.2.15" + "postcss": "^8.5.10", + "rolldown": "1.0.0-rc.17", + "tinyglobby": "^0.2.16" }, "bin": { "vite": "bin/vite.js" diff --git a/test/common.sh b/test/common.sh index 8d8bb1ce..84151577 100755 --- a/test/common.sh +++ b/test/common.sh @@ -118,6 +118,39 @@ run_test_expect_success() { CURRENT_REPO="" } +# resolve_keygen() — print the available key generation tool: "openssl", "ssh-keygen", or "" +resolve_keygen() { + if command -v openssl &>/dev/null; then + echo "openssl" + elif command -v ssh-keygen &>/dev/null; then + echo "ssh-keygen" + else + echo "" + fi +} + +# generate_rsa_key() — write a PKCS#1 RSA-2048 private key (BEGIN RSA PRIVATE KEY) to $1 +generate_rsa_key() { + local outfile="$1" + case "$(resolve_keygen)" in + openssl) openssl genrsa 2048 2>/dev/null > "${outfile}" ;; + ssh-keygen) ssh-keygen -t rsa -b 2048 -N "" -f "${outfile}" -m PEM -q 2>/dev/null + rm -f "${outfile}.pub" ;; + *) return 1 ;; + esac +} + +# generate_pkcs8_key() — write a PKCS#8 private key (BEGIN PRIVATE KEY) to $1 +generate_pkcs8_key() { + local outfile="$1" + case "$(resolve_keygen)" in + openssl) openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 2>/dev/null > "${outfile}" ;; + ssh-keygen) ssh-keygen -t rsa -b 2048 -N "" -f "${outfile}" -m PKCS8 -q 2>/dev/null + rm -f "${outfile}.pub" ;; + *) return 1 ;; + esac +} + # run_orchestrated() — run a test script and aggregate results # Used by *-all.sh scripts to orchestrate multiple subscripts # Args: $1 = test name, $2 = script path diff --git a/test/gitea/env.sh b/test/gitea/env.sh new file mode 100644 index 00000000..ef5cffcf --- /dev/null +++ b/test/gitea/env.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Gitea-local overrides for smoke test scripts. +# Source before running any test script to target the local Compose stack +# instead of the default GitHub remote: +# +# source test/gitea/env.sh +# bash test/push-fail-secrets.sh +# bash test/proxy-fail-secrets.sh +# +# Uses test-user's Gitea token as the git HTTP password so that the proxy's +# identity resolution can call the Gitea API with the same credentials. +# Tokens are written by docker/gitea-setup.sh to test/gitea/tokens.env. + +source "$(dirname "${BASH_SOURCE[0]}")/tokens.env" + +export GIT_USERNAME="me" +export GIT_PASSWORD="${GITEA_TESTUSER_TOKEN}" +export GIT_REPO="gitea/test-owner/test-repo.git" +export GIT_AUTHOR_NAME="test-user" +export GIT_EMAIL="testuser@example.com" diff --git a/test/proxy-fail-secrets.sh b/test/proxy-fail-secrets.sh index 5eb151f6..0cb2155a 100755 --- a/test/proxy-fail-secrets.sh +++ b/test/proxy-fail-secrets.sh @@ -32,19 +32,20 @@ EOF git commit -m "chore: add CI environment config" } -test_private_key_pem() { - # gitleaks rule: private-key — detects PEM-encoded private keys - cat > deploy-key.pem << 'PEMEOF' ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEA2a2rwplBQLzHPtPDSEHbFljEg2kX6BASm1rOBh2cEDAYsNbh -QRFGmGeTKBPs2gJMtaFe0sIliRWAKMq6YIrJTiJNPIqUl/lBOANhwMUwl8n3tMaZ -eDIPDKTpFeJdpMcbh6MqT5QJSjMBb2F3mHB0VBqNpG0JOhRfhm3BQXQM6GQMKACH ------END RSA PRIVATE KEY----- -PEMEOF +test_private_key_rsa() { + # gitleaks rule: private-key — PKCS#1 RSA private key (BEGIN RSA PRIVATE KEY) + generate_rsa_key deploy-key.pem || { echo "SKIP: no key generation tool available"; return 0; } git add deploy-key.pem git commit -m "chore: add deployment key" } +test_private_key_pkcs8() { + # gitleaks rule: private-key — PKCS#8 private key (BEGIN PRIVATE KEY) + generate_pkcs8_key deploy-key-pkcs8.pem || { echo "SKIP: no key generation tool available"; return 0; } + git add deploy-key-pkcs8.pem + git commit -m "chore: add pkcs8 key" +} + test_generic_api_key() { # gitleaks rule: generic-api-key — detects high-entropy strings assigned to key-like variables cat > config.yml << 'EOF' @@ -82,7 +83,8 @@ run_test() { run_test "FAIL: AWS access key in diff" test_aws_access_key run_test "FAIL: GitHub PAT in diff" test_github_pat -run_test "FAIL: RSA private key in diff" test_private_key_pem +run_test "FAIL: RSA private key (PKCS#1)" test_private_key_rsa +run_test "FAIL: RSA private key (PKCS#8)" test_private_key_pkcs8 run_test "FAIL: Generic API key in diff" test_generic_api_key run_test "FAIL: Slack webhook URL in diff" test_slack_webhook diff --git a/test/push-fail-secrets.sh b/test/push-fail-secrets.sh index 166c4819..ab82bc86 100755 --- a/test/push-fail-secrets.sh +++ b/test/push-fail-secrets.sh @@ -32,19 +32,20 @@ EOF git commit -m "chore: add CI environment config" } -test_private_key_pem() { - # gitleaks rule: private-key — detects PEM-encoded private keys - cat > deploy-key.pem << 'PEMEOF' ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEA2a2rwplBQLzHPtPDSEHbFljEg2kX6BASm1rOBh2cEDAYsNbh -QRFGmGeTKBPs2gJMtaFe0sIliRWAKMq6YIrJTiJNPIqUl/lBOANhwMUwl8n3tMaZ -eDIPDKTpFeJdpMcbh6MqT5QJSjMBb2F3mHB0VBqNpG0JOhRfhm3BQXQM6GQMKACH ------END RSA PRIVATE KEY----- -PEMEOF +test_private_key_rsa() { + # gitleaks rule: private-key — PKCS#1 RSA private key (BEGIN RSA PRIVATE KEY) + generate_rsa_key deploy-key.pem || { echo "SKIP: no key generation tool available"; return 0; } git add deploy-key.pem git commit -m "chore: add deployment key" } +test_private_key_pkcs8() { + # gitleaks rule: private-key — PKCS#8 private key (BEGIN PRIVATE KEY) + generate_pkcs8_key deploy-key-pkcs8.pem || { echo "SKIP: no key generation tool available"; return 0; } + git add deploy-key-pkcs8.pem + git commit -m "chore: add pkcs8 key" +} + test_generic_api_key() { # gitleaks rule: generic-api-key — detects high-entropy strings assigned to key-like variables cat > config.yml << 'EOF' @@ -82,7 +83,8 @@ run_test() { run_test "FAIL: AWS access key in diff" test_aws_access_key run_test "FAIL: GitHub PAT in diff" test_github_pat -run_test "FAIL: RSA private key in diff" test_private_key_pem +run_test "FAIL: RSA private key (PKCS#1)" test_private_key_rsa +run_test "FAIL: RSA private key (PKCS#8)" test_private_key_pkcs8 run_test "FAIL: Generic API key in diff" test_generic_api_key run_test "FAIL: Slack webhook URL in diff" test_slack_webhook