diff --git a/.claude/projects/-Users-dev-Documents-Projects-Leakwatch/memory/feedback_commit_docs.md b/.claude/projects/-Users-dev-Documents-Projects-Leakwatch/memory/feedback_commit_docs.md deleted file mode 100644 index 234290c..0000000 --- a/.claude/projects/-Users-dev-Documents-Projects-Leakwatch/memory/feedback_commit_docs.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Always update docs before committing -description: User requires all documentation to be reviewed and updated before any commit — no outdated or incomplete docs allowed -type: feedback ---- - -Before committing code changes, ALWAYS check and update all related documentation first. The user does not want any commit that leaves docs outdated or with incomplete information. - -**Why:** The user has strict documentation standards and wants the repo to be consistent at every commit point — not just code-complete but docs-complete. - -**How to apply:** Before running `git commit`, audit README.md, CLAUDE.md, CHANGELOG.md, roadmap, guides, and any other docs that reference counts, versions, features, or capabilities that may have changed. Update them in the same commit. diff --git a/.github/secret_scanning.yml b/.github/secret_scanning.yml index 00b3236..839d9ad 100644 --- a/.github/secret_scanning.yml +++ b/.github/secret_scanning.yml @@ -1,5 +1,6 @@ paths-ignore: - "internal/detector/**/*_test.go" + - "internal/detector/generic/testdata/appsettings.synthetic.json" - "internal/verifier/**/*_test.go" - "internal/engine/**/*_test.go" - "internal/source/**/*_test.go" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db42ad6..4c4a185 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -110,12 +110,23 @@ jobs: go-version: '1.25.12' cache: true cache-dependency-path: tools/site-build/go.sum - - name: Regenerate site/js from tools/site-build + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '22' + - name: Test site generator and browser detector contracts + working-directory: tools/site-build + run: go test -count=1 ./... + - name: Regenerate site output from tools/site-build working-directory: tools/site-build run: go run . -strict - - name: Fail if the committed site/js has drifted from the generator + - name: Fail if the committed site has drifted from the generator run: | - if ! git diff --exit-code -- site/js; then - echo "::error::site/js is stale. Run 'cd tools/site-build && go run . -strict' from the repo root and commit the regenerated files under site/js/." + if ! git diff --exit-code -- site; then + echo "::error::site output is stale. Run 'cd tools/site-build && go run . -strict' from the repo root and commit the regenerated files under site/." + exit 1 + fi + if test -n "$(git status --porcelain --untracked-files=all -- site)"; then + echo "::error::site generator created untracked output. Regenerate and commit every generated file under site/." + git status --short --untracked-files=all -- site exit 1 fi diff --git a/.github/workflows/provider-contract-audit.yml b/.github/workflows/provider-contract-audit.yml new file mode 100644 index 0000000..a97aa79 --- /dev/null +++ b/.github/workflows/provider-contract-audit.yml @@ -0,0 +1,43 @@ +name: Provider Contract Audit Freshness + +on: + schedule: + - cron: '17 6 * * 1' + workflow_dispatch: + push: + branches: [main] + paths: + - 'internal/meta/capabilities.go' + - 'internal/meta/capabilities_test.go' + - 'internal/meta/provider_contract_links_test.go' + - 'docs/standards/05-PROVIDER-CONTRACT-AUDITS.md' + - '.github/workflows/provider-contract-audit.yml' + pull_request: + paths: + - 'internal/meta/capabilities.go' + - 'internal/meta/capabilities_test.go' + - 'internal/meta/provider_contract_links_test.go' + - 'docs/standards/05-PROVIDER-CONTRACT-AUDITS.md' + - '.github/workflows/provider-contract-audit.yml' + +permissions: + contents: read + +jobs: + freshness: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + with: + persist-credentials: false + - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + with: + go-version: '1.25.12' + cache: true + - name: Enforce reviewed provider-contract freshness + run: go test ./internal/meta -run '^TestVerificationCapabilities_ReviewedContractsAreFresh$' -count=1 + - name: Verify official provider references and anchors + env: + LEAKWATCH_CHECK_PROVIDER_LINKS: '1' + run: go test ./internal/meta -run '^TestVerificationCapabilities_ReviewedReferenceLinksResolve$' -count=1 -timeout=8m diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 73df920..dd5e97a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,6 +29,19 @@ jobs: - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 with: go-version: '1.25.12' + - name: Validate release tag, metadata, and generated site + env: + RELEASE_TAG: ${{ github.ref_name }} + run: | + set -euo pipefail + go run ./internal/meta/releasecheck --tag "$RELEASE_TAG" + go test ./internal/meta ./cmd + ( + cd tools/site-build + go run . -strict + ) + git diff --exit-code -- site + test -z "$(git status --porcelain --untracked-files=all -- site)" # docker/setup-qemu-action@v3.2.0 - name: Set up QEMU uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf diff --git a/.github/workflows/site-deploy.yml b/.github/workflows/site-deploy.yml index d68ebc7..ff94e56 100644 --- a/.github/workflows/site-deploy.yml +++ b/.github/workflows/site-deploy.yml @@ -6,6 +6,7 @@ on: paths: - "site/**" - "docs/user-manuals/**" + - "internal/meta/release.go" - "tools/site-build/**" - ".github/workflows/site-deploy.yml" workflow_dispatch: diff --git a/.github/workflows/vscode-ci.yml b/.github/workflows/vscode-ci.yml index c611eb7..aa80be5 100644 --- a/.github/workflows/vscode-ci.yml +++ b/.github/workflows/vscode-ci.yml @@ -53,3 +53,14 @@ jobs: - name: Test run: npm test + + - name: Package VSIX + run: npm run package:vsix + + - name: Upload VSIX + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: leakwatch-vsix + path: vscode/leakwatch-*.vsix + if-no-files-found: error + retention-days: 14 diff --git a/.github/workflows/vscode-release.yml b/.github/workflows/vscode-release.yml new file mode 100644 index 0000000..c1bd3e1 --- /dev/null +++ b/.github/workflows/vscode-release.yml @@ -0,0 +1,122 @@ +name: Publish VS Code Extension + +on: + workflow_dispatch: + inputs: + publish: + description: Publish the packaged VSIX to Visual Studio Marketplace + required: true + type: boolean + default: false + +permissions: + contents: read + +concurrency: + group: vscode-marketplace-publish + cancel-in-progress: false + +defaults: + run: + working-directory: vscode + +jobs: + package: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + with: + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '22' + cache: npm + cache-dependency-path: vscode/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Lint, test, and package + run: npm run lint && npm test && npm run package:vsix + + - name: Upload reviewable VSIX + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: leakwatch-vsix + path: vscode/leakwatch-*.vsix + if-no-files-found: error + retention-days: 14 + + publish: + if: ${{ inputs.publish && github.ref == 'refs/heads/main' }} + needs: package + runs-on: ubuntu-latest + timeout-minutes: 15 + environment: vscode-marketplace + permissions: + actions: read + contents: read + deployments: read + steps: + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + with: + persist-credentials: false + + - name: Fail closed unless Marketplace environment policy is protected + env: + GH_API_TOKEN: ${{ github.token }} + GH_ENVIRONMENT_AUDIT_TOKEN: ${{ secrets.VSCODE_ENV_AUDIT_TOKEN }} + run: | + policy_file="${RUNNER_TEMP:?}/vscode-marketplace-environment.json" + curl --fail-with-body --silent --show-error \ + --header "Accept: application/vnd.github+json" \ + --header @<(printf 'Authorization: Bearer %s\n' "${GH_API_TOKEN:?}") \ + --header "X-GitHub-Api-Version: 2022-11-28" \ + "${GITHUB_API_URL:?}/repos/${GITHUB_REPOSITORY:?}/environments/vscode-marketplace" \ + --output "$policy_file" + jq --exit-status ' + ([.protection_rules[]? + | select(.type == "required_reviewers" and .prevent_self_review == true) + | .reviewers[]?] | length) >= 1 + and .deployment_branch_policy.protected_branches == true + ' "$policy_file" >/dev/null + + secret_metadata_file="${RUNNER_TEMP:?}/vscode-marketplace-vsce-pat.json" + curl --fail-with-body --silent --show-error \ + --header "Accept: application/vnd.github+json" \ + --header @<(printf 'Authorization: Bearer %s\n' "${GH_ENVIRONMENT_AUDIT_TOKEN:?}") \ + --header "X-GitHub-Api-Version: 2022-11-28" \ + "${GITHUB_API_URL:?}/repos/${GITHUB_REPOSITORY:?}/environments/vscode-marketplace/secrets/VSCE_PAT" \ + --output "$secret_metadata_file" + jq --exit-status '.name == "VSCE_PAT"' "$secret_metadata_file" >/dev/null + + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '22' + cache: npm + cache-dependency-path: vscode/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Download reviewed VSIX + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: leakwatch-vsix + path: vscode/artifact + + - name: Publish reviewed VSIX + env: + VSCE_PAT: ${{ secrets.VSCE_PAT }} + run: | + : "${VSCE_PAT:?missing environment-scoped VSCE_PAT}" + mapfile -t packages < <(find artifact -type f -name 'leakwatch-*.vsix' -print) + if [[ ${#packages[@]} -ne 1 ]]; then + echo "expected exactly one VSIX artifact" >&2 + exit 1 + fi + npx --no-install vsce publish --packagePath "${packages[0]}" diff --git a/.gitignore b/.gitignore index aa2c11c..8a93dac 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ vendor/ # IDE .idea/ .vscode/ +.claude/ *.swp *.swo *~ @@ -30,6 +31,7 @@ Thumbs.db # Code Reviews docs/code-reviews/ +review/ # Marketing (internal, not published) docs/marketing/ diff --git a/.leakwatchignore b/.leakwatchignore index 42edd56..cf47403 100644 --- a/.leakwatchignore +++ b/.leakwatchignore @@ -2,6 +2,10 @@ # These are NOT real secrets — they are fixtures that match regex patterns. **/*_test.go +# This fixture contains deterministic, synthetic, intentionally matchable +# credentials. Keep the exemption exact so future testdata remains scanned. +internal/detector/generic/testdata/appsettings.synthetic.json + # Verifier implementations contain API URL patterns that may match detectors. internal/verifier/** diff --git a/CHANGELOG.md b/CHANGELOG.md index c97ae84..30e15f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,49 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] -_Nothing yet._ +## [v1.8.0] - 2026-08-11 + +### Added + +- A default-on `structured-config-secret` detector for JSON, YAML, TOML, INI, + dotenv, and XML configuration values, bringing the catalog to **65 + detectors**. It can be disabled explicitly with + `--exclude-detectors structured-config-secret`. +- Repeatable `--verifier-origin detector-id=https://host` routing for + operator-trusted verifier targets, plus the backward-compatible + `--grafana-instance-url` alias. +- Opt-in Slack text-attachment scanning with Slack-owned HTTPS enforcement, + operation-scoped request limiting, byte-bounded backpressure, bounded file + size/retries, UTF-8 text classification, token-safe errors/logs, and file-ID + de-duplication. +- Best-effort GitHub scope/expiration and GitLab PAT standard or GitLab 19.2+ granular scope/expiration metadata + after authoritative active verification. +- A reviewable VSIX artifact, 256×256 Marketplace icon, main-only manual VS Code + publication workflow with executable environment-policy and environment-secret + provenance audits, and a scheduled provider-contract freshness/link gate. + +### Changed + +- Auth0, Datadog, GitHub, GitHub OAuth, GitLab, Grafana, Shopify, Snyk, and + Twilio verification now requires an explicit operator-trusted origin. Without + one, Leakwatch sends no credential and reports the finding as `unverified`. +- Slack's `--rate-limit` default changed from `1` to `0`; zero preserves the + provider-safe, operation-specific limits instead of imposing one global + request rate on every Slack method. +- Source cleanup or partial-scan failures are now visible and non-clean. A + cleanup failure without findings exits `2`; partial results still exit `1` + while retaining and reporting the source error. +- Finding JSON now distinguishes calculated zero entropy from absent entropy and + omits an unset detection timestamp; terminal tables use Unicode display width, + visibly escape bidi controls, and render `--show-raw` as a reversible quoted + value instead of passing terminal controls through or dropping secret bytes. +- Detector/verifier registries fail closed on invalid or duplicate registration, + matcher results are explicitly stable, and obsolete global custom-rule APIs + were removed. --- -## [v1.7.0] - 2026-07-19 +## [v1.7.0] - 2026-07-20 > A comprehensive, multi-dimensional review of the entire project (code, tests, docs, CI/CD, VS Code extension, and website) was completed and its findings remediated across the changes below. diff --git a/CLAUDE.md b/CLAUDE.md index be1ef43..4531a49 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -66,11 +66,12 @@ Architecture decisions are documented in ADR format under `docs/decisions/`. The | [ADR-0002](docs/decisions/ADR-0002-cli-frame.md) | Cobra + Viper | Nested commands, hierarchical configuration | | [ADR-0003](docs/decisions/ADR-0003-git-library.md) | go-git | Pure Go, no CGO, no external dependencies | | [ADR-0004](docs/decisions/ADR-0004-plugin-architecture.md) | Compile-time registration | init() + blank import, type-safe | -| [ADR-0005](docs/decisions/ADR-0005-pattern-matching.md) | Aho-Corasick hybrid | AC pre-filter → regex validation → entropy | +| [ADR-0005](docs/decisions/ADR-0005-pattern-matching.md) | Aho-Corasick hybrid | Entropy clause amended; AC pre-filter → regex validation remains active | | [ADR-0006](docs/decisions/ADR-0006-container-library.md) | go-containerregistry | Daemonless, layer-based analysis | | [ADR-0007](docs/decisions/ADR-0007-license.md) | MIT | Enterprise adoption, open-core compatibility | | [ADR-0008](docs/decisions/ADR-0008-concurrency-model.md) | Worker Pool | Fixed worker count, channel-based | | [ADR-0009](docs/decisions/ADR-0009-github-marketplace-action.md) | Marketplace Action | Root `action.yml`, prebuilt-binary composite, `@v1` | +| [ADR-0010](docs/decisions/ADR-0010-entropy-gating-policy.md) | Entropy gating | Threshold gates opt-in heuristic findings only | ## Coding Standards @@ -124,6 +125,11 @@ Full standards: [docs/standards/04-DEVELOPMENT-STANDARDS.md](docs/standards/04-D Types: feat, fix, docs, test, refactor, perf, ci, chore ``` +Before every commit, review the documentation surfaces affected by the change +(including README, changelog, roadmap, guides, user manuals, and generated site +artifacts). A commit must not knowingly leave product behavior, counts, versions, +or capability claims stale. + **Examples:** ``` feat(detector): add AWS Secret Access Key detector diff --git a/README.md b/README.md index b1abe3e..5d5a9a6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@
-Leakwatch — detect, verify & report leaked secrets +Leakwatch — detect, verify & report leaked secrets **Detect, verify & report leaked secrets across code, Git history, containers, and the cloud.** Open source (MIT) · single binary · built for CI. @@ -19,7 +19,7 @@ Open source (MIT) · single binary · built for CI. ## What is Leakwatch? -Leaked API keys, tokens, and passwords are one of the most common causes of breaches. **Leakwatch** finds them across your **codebase, full Git history, container images, and cloud storage** — and then *verifies whether each secret is still live*, so you spend time on real incidents instead of triaging noise. +Leaked API keys, tokens, and passwords are one of the most common causes of breaches. **Leakwatch** finds them across your **codebase, full Git history, container images, and cloud storage** — and, where the provider contract and required context allow it, *checks whether a secret is still live*, so you can prioritize confirmed incidents without hiding unverified risk. ```console $ leakwatch scan fs . @@ -56,8 +56,8 @@ $ leakwatch scan fs . ## Features - **6 scan sources** — filesystem, Git history (every commit), container images, AWS S3, Google Cloud Storage, Slack -- **64 built-in detectors** + **YAML custom rules** (no Go code needed) -- **54 live verifiers (84.4%)** — confirms whether a secret is *still active*, not just present +- **65 built-in detectors** + **YAML custom rules** (no Go code needed) +- **39 direct live checks** + **9 context-required checks** + **6 offline format validators** — registry presence is never overstated as live capability - **5 output formats** — JSON, SARIF, CSV, terminal table, and **GitHub inline annotations** - **Drop-in distribution** — GitHub Action (Marketplace), Docker image, Homebrew, `go install`, single static binary - **Secret-safe** — redacted output by default; secrets are never logged or stored @@ -118,26 +118,27 @@ Add secret scanning to any workflow in one line — published on the [GitHub Mar Exit codes (used for CI gating): **`0`** no findings · **`1`** findings reported · **`2`** error · **`3`** interrupted (Ctrl-C/SIGTERM). -Full inputs and recipes: **[CI/CD Integration guide](docs/guides/ci-cd-integration.md)**. +Full inputs and recipes: **[CI/CD manual](docs/user-manuals/en/ci-cd/github-action.md)**. ## Is it still live? -Detection is only half the job — a key that was already rotated isn't an incident. For most secret types, Leakwatch makes a **controlled, read-only API call** to the provider to confirm status: +Detection is only half the job — a key that was already rotated isn't an incident. For most secret types, Leakwatch makes a **controlled, non-destructive provider check** to confirm status: | Tier | What it means | Coverage | |------|---------------|----------| -| **Live verified** | Read-only API call confirms the key is active / inactive | ~48 detectors | +| **Direct live** | Non-destructive provider check can confirm the key in the normal production path | 39 detectors | +| **Context required** | A safe issuer, region, or paired credential must be supplied first | 9 detectors | | **Format checked** | Structurally validated where no safe live check exists | 6 detectors | -| **Not verifiable** | No public API (e.g. JWTs, private keys) — detected & triaged manually | 10 detectors | +| **Not verifiable** | No safe provider check (e.g. JWTs, private keys) — detected & triaged manually | 11 detectors | -That's **54 of 64 detectors (84.4%)** with verification. Verification is on by default for the CLI and off by default in the Action (to keep CI fast and offline) — flip it with `no-verify`. +Leakwatch registers verifier implementations for **54 of 65 detectors (83.1%)**, but only **39** are direct live checks. The remaining registered implementations are context-required or format-only and therefore must not be interpreted as proof of live coverage. Verification is on by default for the CLI and off by default in the Action (to keep CI fast and offline) — flip it with `no-verify`. ## Why Leakwatch? | | **Leakwatch** | TruffleHog | Gitleaks | |---|---|---|---| | License | **MIT** | AGPL-3.0 | MIT [^gl] | -| Live secret verification | **Yes (54 verifiers, 84.4%)** | Yes | No | +| Live secret verification | **Yes (39 direct-live; 9 context-required)** | Yes | No | | Container image scanning | **Yes** | Yes | No | | Cloud sources (S3 / GCS / Slack) | **Yes** | No | No | | SARIF output | **Yes** | No [^th] | Yes | @@ -151,7 +152,7 @@ That's **54 of 64 detectors (84.4%)** with verification. Verification is on by d ## Detectors -**64 built-in detectors** across these categories, plus your own [YAML custom rules](docs/guides/custom-rules.md): +**65 built-in detectors** across these categories, plus your own [YAML custom rules](docs/user-manuals/en/detectors/custom-rules.md): | Category | Examples | |----------|----------| @@ -167,7 +168,7 @@ That's **54 of 64 detectors (84.4%)** with verification. Verification is on by d | **Generic & Custom** | high-entropy generic keys · LaunchDarkly · SonarCloud · your YAML rules |
-Full detector catalog (64) with IDs, severity & verification +Full detector catalog (65) with IDs, severity & verification | Category | Detector | ID | Severity | |----------|----------|----|----------| @@ -210,7 +211,7 @@ That's **54 of 64 detectors (84.4%)** with verification. Verification is on by d | Database | Redis Connection | `redis-connection-string` | Critical | | Database | Snowflake Credentials | `snowflake-credentials` | Critical | | Database | RabbitMQ Connection | `rabbitmq-connection-string` | Critical | -| Database | Supabase Service Key | `supabase-service-key` | Critical | +| DevTools | Supabase Personal Access Token | `supabase-service-key` | Critical | | Infrastructure | FTP/SFTP Credentials | `ftp-credentials` | Critical | | Infrastructure | LDAP Credentials | `ldap-credentials` | Critical | | Infrastructure | Databricks PAT | `databricks-token` | Critical | @@ -225,7 +226,7 @@ That's **54 of 64 detectors (84.4%)** with verification. Verification is on by d | Monitoring | New Relic API Key | `newrelic-api-key` | High | | Monitoring | Sentry Auth Token | `sentry-token` | High | | Security | Snyk API Key | `snyk-api-key` | High | -| Security | Twilio API Key | `twilio-api-key` | Critical | +| Security | Twilio API Key Secret | `twilio-api-key` | Critical | | Secrets Mgmt | Doppler Service Token | `doppler-token` | Critical | | Feature Flags | LaunchDarkly SDK Key | `launchdarkly-sdk-key` | High | | Code Quality | SonarCloud Token | `sonarcloud-token` | High | @@ -235,6 +236,7 @@ That's **54 of 64 detectors (84.4%)** with verification. Verification is on by d | SaaS | Figma PAT | `figma-pat` | High | | SaaS | Airtable PAT | `airtable-pat` | High | | Generic | Generic API Key | `generic-api-key` | Medium | +| Generic | Structured Configuration Secret | `structured-config-secret` | High |
@@ -270,12 +272,12 @@ output: show-raw: false ``` -Use `.leakwatchignore` and `# leakwatch:ignore` markers to suppress known false positives. Details: **[Configuration guide](docs/guides/configuration.md)**. +Use `.leakwatchignore` and `# leakwatch:ignore` markers to suppress known false positives. Details: **[Ignoring findings](docs/user-manuals/en/configuration/ignoring-findings.md)**. ## Security - Secret values are **redacted by default** (e.g. `AKIA****MPLE`) and are **never written to disk or logs**. The raw value is only emitted if you explicitly pass `--show-raw`. -- Verification uses **controlled, read-only** API calls to providers; it makes no state-changing requests. +- Verification uses **controlled, non-destructive** provider checks; it makes no state-changing requests. - Found a vulnerability? Please report it privately via a [GitHub security advisory](https://github.com/HodeTech/Leakwatch/security/advisories/new). ## Architecture @@ -295,9 +297,10 @@ flowchart LR E2["Regex"] E3["Shannon entropy"] end - subgraph Verify["Verification (54 verifiers)"] - V1["Live API"] - V2["Format validation"] + subgraph Verify["Verification (54 registered implementations)"] + V1["39 direct live"] + V2["9 context-required"] + V3["6 format-only"] end Sources -->|chunks| Engine Engine -->|findings| Verify @@ -308,19 +311,15 @@ Deep dive: [Architecture](docs/architecture/03-ARCHITECTURE.md) · [ADRs](docs/d ## Documentation -Full bilingual (EN/TR) manuals are at **[hodetech.github.io/Leakwatch](https://hodetech.github.io/Leakwatch/)**. Quick links: - -[Getting Started](docs/guides/getting-started.md) · -[Configuration](docs/guides/configuration.md) · -[CI/CD](docs/guides/ci-cd-integration.md) · -[Custom Rules](docs/guides/custom-rules.md) · -[Container Scanning](docs/guides/container-scanning.md) · -[Cloud Scanning](docs/guides/cloud-scanning.md) · -[Git Scanning](docs/guides/git-scanning.md) · -[Slack Scanning](docs/guides/slack-scanning.md) · -[Verification](docs/guides/secret-verification.md) · -[Docker](docs/guides/docker-usage.md) · -[VS Code Extension](docs/guides/vscode-extension.md) · +The bilingual site is at **[hodetech.github.io/Leakwatch](https://hodetech.github.io/Leakwatch/)**. The English user manual is the authoritative product-behavior source; Turkish is its reviewed translation, and the site bundles are generated from both. + +[Introduction](docs/user-manuals/en/getting-started/introduction.md) · +[Installation](docs/user-manuals/en/getting-started/installation.md) · +[Configuration](docs/user-manuals/en/configuration/config-file.md) · +[CLI Reference](docs/user-manuals/en/reference/cli-reference.md) · +[Verification](docs/user-manuals/en/verification/how-verification-works.md) · +[Detector Catalog](docs/user-manuals/en/detectors/detector-catalog.md) · +[Supplemental Guides](docs/guides/README.md) · [Roadmap](docs/05-ROADMAP.md) ## Contributing diff --git a/cmd/imports.go b/cmd/imports.go index eac7220..337c7ba 100644 --- a/cmd/imports.go +++ b/cmd/imports.go @@ -29,7 +29,7 @@ import ( _ "github.com/HodeTech/leakwatch/internal/detector/figma" // register figma detector _ "github.com/HodeTech/leakwatch/internal/detector/ftp" // register ftp credentials detector _ "github.com/HodeTech/leakwatch/internal/detector/gcp" // register gcp service-account detector - _ "github.com/HodeTech/leakwatch/internal/detector/generic" // register generic api-key detector + _ "github.com/HodeTech/leakwatch/internal/detector/generic" // register generic API-key and structured-config detectors _ "github.com/HodeTech/leakwatch/internal/detector/github" // register github detectors (pat + oauth) _ "github.com/HodeTech/leakwatch/internal/detector/gitlab" // register gitlab detector _ "github.com/HodeTech/leakwatch/internal/detector/grafana" // register grafana detector diff --git a/cmd/init.go b/cmd/init.go index 8e6b0b7..bb5d05e 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -7,8 +7,11 @@ import ( "log/slog" "os" "path/filepath" + "strings" "github.com/spf13/cobra" + + "github.com/HodeTech/leakwatch/internal/meta" ) var initCmd = &cobra.Command{ @@ -32,7 +35,7 @@ func init() { initCmd.Flags().Bool("force", false, "overwrite existing config file") } -const defaultConfig = `# Leakwatch Configuration +const defaultConfigTemplate = `# Leakwatch Configuration # Documentation: https://github.com/HodeTech/Leakwatch/blob/main/docs/guides/configuration.md scan: @@ -47,10 +50,10 @@ detection: verification: enabled: true # Enable secret verification via API rate-limit: 10 # Max verification requests per second - timeout: 10s # Per-request timeout + timeout: 10s # Per-finding verification-operation timeout output: - format: json # Output format: json, sarif, csv, table, github + format: json # Output format: {{OUTPUT_FORMATS}} show-raw: false # Never show raw secret values filter: @@ -72,6 +75,8 @@ filter: # severity: critical ` +var defaultConfig = strings.Replace(defaultConfigTemplate, "{{OUTPUT_FORMATS}}", meta.OutputFormatList, 1) + func runInit(cmd *cobra.Command, _ []string) error { outputPath, err := cmd.Flags().GetString("output") if err != nil { diff --git a/cmd/root.go b/cmd/root.go index a0a67f6..2490ffc 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -9,6 +9,8 @@ import ( "os" "github.com/spf13/cobra" + + "github.com/HodeTech/leakwatch/internal/meta" ) var ( @@ -49,19 +51,7 @@ func (e *InterruptedExitError) Error() string { var rootCmd = &cobra.Command{ Use: "leakwatch", Short: "Detects leaked secrets in codebases", - Long: `Leakwatch is a high-performance security tool that detects, verifies, and reports -leaked secrets (API keys, passwords, certificates) in codebases, Git histories, -container images, cloud storage buckets, and Slack workspaces. - -Features: - - 63 built-in secret detectors (60 packages) covering AWS, GitHub, Slack, Stripe, JWT, and more - - 54 verification checks to confirm whether discovered secrets are active - - Scans filesystems, Git repos, container images, S3, GCS, and Slack - - Multiple output formats: JSON, SARIF, CSV, and terminal table - - Aho-Corasick pre-filtering for fast multi-pattern matching - - Concurrent worker pool architecture for high throughput - - Custom rules via YAML configuration - - .leakwatchignore and inline ignore support`, + Long: rootLongDescription(), Example: ` # Quick scan of current directory leakwatch scan fs . @@ -80,6 +70,30 @@ Features: SilenceErrors: true, } +func rootLongDescription() string { + capabilities := meta.VerificationCapabilityCounts() + return fmt.Sprintf(`Leakwatch is a high-performance security tool that detects, verifies, and reports +leaked secrets (API keys, passwords, certificates) in codebases, Git histories, +container images, cloud storage buckets, and Slack workspaces. + +Features: + - %d built-in secret detectors covering AWS, GitHub, Slack, Stripe, JWT, and more + - %d verification implementations: %d direct-live, %d context-required, %d format-only + - Scans filesystems, Git repos, container images, S3, GCS, and Slack + - Multiple output formats: %s + - Aho-Corasick pre-filtering for fast multi-pattern matching + - Concurrent worker pool architecture for high throughput + - Custom rules via YAML configuration + - .leakwatchignore and inline ignore support`, + meta.Detectors, + meta.Verifiers, + capabilities.Live, + capabilities.RequiresContext, + capabilities.FormatOnly, + meta.OutputFormatList, + ) +} + // Execute runs the root command and returns the process exit code: // // 0 clean, completed scan with no findings @@ -90,17 +104,12 @@ func Execute() int { // ExecuteC returns the command that actually ran/failed so the error hint can // point at that subcommand's own --help rather than the top-level one. cmd, err := rootCmd.ExecuteC() - if err == nil { - return 0 + code := exitCodeForError(err) + if code == 0 || code == 1 { + return code } - var fErr *FindingsExitError - if errors.As(err, &fErr) { - return 1 - } - - var iErr *InterruptedExitError - if errors.As(err, &iErr) { + if code == 3 { fmt.Fprintf(os.Stderr, "Error: %s\n", err) slog.Debug("scan interrupted", "error", err) return 3 @@ -113,6 +122,23 @@ func Execute() int { return 2 } +// exitCodeForError is the single testable mapping between typed command errors +// and the process contract documented above. +func exitCodeForError(err error) int { + if err == nil { + return 0 + } + var findingsErr *FindingsExitError + if errors.As(err, &findingsErr) { + return 1 + } + var interruptedErr *InterruptedExitError + if errors.As(err, &interruptedErr) { + return 3 + } + return 2 +} + func init() { // Config discovery is performed per-command in an isolated Viper instance // (see newScanViper in scan_common.go); the global Viper is intentionally not diff --git a/cmd/scan_common.go b/cmd/scan_common.go index 2d74071..9250731 100644 --- a/cmd/scan_common.go +++ b/cmd/scan_common.go @@ -19,6 +19,7 @@ import ( "github.com/HodeTech/leakwatch/internal/config" "github.com/HodeTech/leakwatch/internal/engine" + "github.com/HodeTech/leakwatch/internal/meta" "github.com/HodeTech/leakwatch/internal/output" csvout "github.com/HodeTech/leakwatch/internal/output/csv" githubout "github.com/HodeTech/leakwatch/internal/output/github" @@ -39,7 +40,11 @@ const defaultMaxFileSize = 10 * 1024 * 1024 // Flag names shared across the scan_*.go commands. Defining them as constants // avoids duplicating the string literals that several commands reference when // registering and reading the same flag. -const flagShowRaw = "show-raw" +const ( + flagShowRaw = "show-raw" + flagGrafanaInstanceURL = "grafana-instance-url" + flagVerifierOrigin = "verifier-origin" +) // scanFlagBindings maps Viper config keys to the scan flag that overrides them. // Each scan command's pflags are bound to a fresh, per-invocation Viper instance @@ -129,7 +134,7 @@ func newScanViper(cmd *cobra.Command) (*viper.Viper, error) { // new subcommand cannot drift out of sync with the common flag surface, and it is // the single place the --max-file-size default is wired. func addCommonScanFlags(flags *pflag.FlagSet) { - flags.StringP("format", "f", "json", "output format (json, sarif, csv, table, github)") + flags.StringP("format", "f", "json", "output format ("+meta.OutputFormatList+")") flags.StringP("output", "o", "", "output file (default: stdout)") flags.IntP("concurrency", "c", runtime.NumCPU(), "number of concurrent workers") flags.Int64("max-file-size", defaultMaxFileSize, "maximum file size in bytes") @@ -145,13 +150,15 @@ func addExcludePathFlag(flags *pflag.FlagSet) { flags.StringSlice("exclude", nil, "path patterns to exclude") } -// addVerifyFlags adds --no-verify, --only-verified, --min-severity and -// --remediation flags. +// addVerifyFlags adds the shared verification, severity, remediation, and +// command-line-only trusted-origin flags. func addVerifyFlags(flags *pflag.FlagSet) { flags.Bool("no-verify", false, "disable secret verification") flags.Bool("only-verified", false, "only show verified active findings") flags.String("min-severity", "low", "minimum severity to report (low, medium, high, critical)") flags.Bool("remediation", false, "include remediation guidance in output") + flags.String(flagGrafanaInstanceURL, "", "trusted Grafana instance origin for token verification (HTTPS; command-line only)") + flags.StringArray(flagVerifierOrigin, nil, "trusted verifier origin as detector-id=https://host (repeatable; command-line only)") } // loadScanConfig loads and validates configuration for the active command using @@ -159,8 +166,8 @@ func addVerifyFlags(flags *pflag.FlagSet) { // guarantees that flags such as --concurrency, --max-file-size, and --format // honor flag > env > config-file > default precedence without picking up another // scan command's flag defaults (SYS-07a/b). Flags that are not config-keyed -// (--no-verify, --only-verified, --min-severity, --remediation, --exclude) are -// read directly from the command. +// (--no-verify, --only-verified, --min-severity, --remediation, --exclude, and +// trusted verifier origins) are read directly from the command. func loadScanConfig(cmd *cobra.Command) (*scanner.Config, error) { v, err := newScanViper(cmd) if err != nil { @@ -191,29 +198,56 @@ func loadScanConfig(cmd *cobra.Command) (*scanner.Config, error) { if cmd.Flags().Changed(flagShowRaw) { showRaw = flagBool(cmd, flagShowRaw) } + trustedOrigins, err := trustedVerifierOriginsFromFlags(cmd) + if err != nil { + return nil, err + } return &scanner.Config{ - Concurrency: cfg.Scan.Concurrency, - MaxFileSize: cfg.Scan.MaxFileSize, - ExcludePaths: cfg.Filter.ExcludePaths, - ExcludeDetectors: mergedExcludeDetectors(cmd, cfg), - EnableEntropy: cfg.Detection.Entropy.Enabled, - EntropyThreshold: cfg.Detection.Entropy.Threshold, - ShowRaw: showRaw, - OutputFile: cfg.Output.File, - Format: cfg.Output.Format, - NoVerify: flagBool(cmd, "no-verify"), - OnlyVerified: flagBool(cmd, "only-verified"), - MinSeverity: minSev, - EnableRemediation: flagBool(cmd, "remediation"), - VerifyEnabled: cfg.Verification.Enabled, - VerifyTimeout: cfg.Verification.Timeout, - VerifyConcurrency: cfg.Verification.Concurrency, - VerifyRateLimit: cfg.Verification.RateLimit, - CustomRules: cfg.CustomRules, + Concurrency: cfg.Scan.Concurrency, + MaxFileSize: cfg.Scan.MaxFileSize, + ExcludePaths: cfg.Filter.ExcludePaths, + ExcludeDetectors: mergedExcludeDetectors(cmd, cfg), + EnableEntropy: cfg.Detection.Entropy.Enabled, + EntropyThreshold: cfg.Detection.Entropy.Threshold, + ShowRaw: showRaw, + OutputFile: cfg.Output.File, + Format: cfg.Output.Format, + NoVerify: flagBool(cmd, "no-verify"), + OnlyVerified: flagBool(cmd, "only-verified"), + MinSeverity: minSev, + EnableRemediation: flagBool(cmd, "remediation"), + VerifyEnabled: cfg.Verification.Enabled, + VerifyTimeout: cfg.Verification.Timeout, + VerifyConcurrency: cfg.Verification.Concurrency, + VerifyRateLimit: cfg.Verification.RateLimit, + GrafanaInstanceURL: flagString(cmd, flagGrafanaInstanceURL), + TrustedVerifierOrigins: trustedOrigins, + CustomRules: cfg.CustomRules, }, nil } +func trustedVerifierOriginsFromFlags(cmd *cobra.Command) (map[string]string, error) { + values := flagStringArray(cmd, flagVerifierOrigin) + if len(values) == 0 { + return nil, nil + } + origins := make(map[string]string, len(values)) + for _, value := range values { + detectorID, origin, ok := strings.Cut(value, "=") + detectorID = strings.TrimSpace(detectorID) + origin = strings.TrimSpace(origin) + if !ok || detectorID == "" || origin == "" { + return nil, fmt.Errorf("invalid --%s value %q: expected detector-id=https://host", flagVerifierOrigin, value) + } + if _, duplicate := origins[detectorID]; duplicate { + return nil, fmt.Errorf("duplicate --%s entry for detector %q", flagVerifierOrigin, detectorID) + } + origins[detectorID] = origin + } + return origins, nil +} + // mergedExcludeDetectors returns the config-file filter.exclude-detectors // combined with any per-invocation --exclude-detectors flag values. Detector // exclusion is otherwise config-file-only; wiring the flag here lets an operator @@ -249,12 +283,13 @@ func mergedExcludePaths(cmd *cobra.Command, cfg *scanner.Config) []string { // runScan wires a single-source scan: it installs SIGINT/SIGTERM handling, // delegates the scan pipeline to internal/scanner, renders the result, and maps // an interruption to a distinct non-zero exit. If cl is non-nil its Close is -// called (best-effort) when the scan completes. -func runScan(cmd *cobra.Command, cfg *scanner.Config, src source.Source, cl io.Closer) error { +// called when the scan completes; cleanup failures are returned to the caller. +func runScan(cmd *cobra.Command, cfg *scanner.Config, src source.Source, cl io.Closer) (retErr error) { if cl != nil { defer func() { if err := cl.Close(); err != nil { slog.Warn("failed to clean up source", "error", err) + retErr = errors.Join(retErr, fmt.Errorf("failed to clean up source: %w", err)) } }() } @@ -286,7 +321,12 @@ func finishScan(cfg *scanner.Config, result *engine.ScanResult, sourceType strin } if len(result.Findings) > 0 { - return &FindingsExitError{Count: len(result.Findings)} + findingsErr := &FindingsExitError{Count: len(result.Findings)} + if scanErr != nil { + slog.Warn("scan produced partial results before a source failure") + return errors.Join(findingsErr, fmt.Errorf("scan failed after partial results: %w", scanErr)) + } + return findingsErr } if result.Interrupted { slog.Warn("scan did not complete before interruption; results are partial", "error", scanErr) @@ -488,3 +528,13 @@ func flagStringSlice(cmd *cobra.Command, name string) []string { } return v } + +// flagStringArray reads a repeatable string-array flag; see flagString for the +// error convention. +func flagStringArray(cmd *cobra.Command, name string) []string { + v, err := cmd.Flags().GetStringArray(name) + if err != nil { + slog.Debug("flag lookup failed", "flag", name, "error", err) + } + return v +} diff --git a/cmd/scan_common_test.go b/cmd/scan_common_test.go index 895e5b6..468f52e 100644 --- a/cmd/scan_common_test.go +++ b/cmd/scan_common_test.go @@ -2,11 +2,15 @@ package cmd import ( "bytes" + "os" + "path/filepath" + "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/HodeTech/leakwatch/internal/meta" csvout "github.com/HodeTech/leakwatch/internal/output/csv" githubout "github.com/HodeTech/leakwatch/internal/output/github" jsonout "github.com/HodeTech/leakwatch/internal/output/json" @@ -85,22 +89,88 @@ func TestSelectFormatter_AllFormats_ReturnsCorrectType(t *testing.T) { } } +func TestSelectFormatter_CanonicalFormatsHaveExactImplementations(t *testing.T) { + expected := map[string]interface{}{ + "json": &jsonout.Formatter{}, + "sarif": &sarifout.Formatter{}, + "csv": &csvout.Formatter{}, + "table": &tableout.Formatter{}, + "github": &githubout.Formatter{}, + } + require.Len(t, expected, len(meta.OutputFormatNames())) + + for _, format := range meta.OutputFormatNames() { + want, ok := expected[format] + require.True(t, ok, "canonical format %q has no formatter contract", format) + assert.IsType(t, want, selectFormatter(format, false, false)) + } + for format := range expected { + assert.True(t, meta.IsOutputFormat(format), "formatter %q is absent from canonical metadata", format) + } +} + func TestRootCommand_VersionFlag_ShowsVersion(t *testing.T) { // Set known version info for deterministic output. SetVersionInfo("1.0.0-test", "abc1234", "2026-03-24") + t.Cleanup(func() { SetVersionInfo("dev", "none", "unknown") }) buf := new(bytes.Buffer) rootCmd.SetOut(buf) rootCmd.SetErr(buf) rootCmd.SetArgs([]string{"version"}) + t.Cleanup(func() { + rootCmd.SetOut(nil) + rootCmd.SetErr(nil) + rootCmd.SetArgs(nil) + }) err := rootCmd.Execute() require.NoError(t, err) - output := buf.String() - assert.Contains(t, output, "1.0.0-test") - assert.Contains(t, output, "abc1234") - assert.Contains(t, output, "2026-03-24") + want := readGolden(t, "version.golden") + assert.Equal(t, want, buf.String()) +} + +func TestRootCommand_HelpMatchesGolden(t *testing.T) { + buf := new(bytes.Buffer) + rootCmd.SetOut(buf) + rootCmd.SetErr(buf) + rootCmd.SetArgs([]string{"--help"}) + t.Cleanup(func() { + rootCmd.SetOut(nil) + rootCmd.SetErr(nil) + rootCmd.SetArgs(nil) + }) + + require.NoError(t, rootCmd.Execute()) + want := readGolden(t, "root-help.golden") + assert.Equal(t, want, buf.String()) +} + +func TestOutputFormats_MatchGoldenAndFlagHelp(t *testing.T) { + want := readGolden(t, "output-formats.golden") + got := strings.Join(meta.OutputFormatNames(), "\n") + "\n" + assert.Equal(t, want, got) + + formatFlag := scanFsCmd.Flags().Lookup("format") + require.NotNil(t, formatFlag) + assert.Equal(t, "output format ("+meta.OutputFormatList+")", formatFlag.Usage) + assert.Equal(t, 1, strings.Count(defaultConfigTemplate, "{{OUTPUT_FORMATS}}")) + assert.Contains(t, defaultConfig, "# Output format: "+meta.OutputFormatList) + assert.NotContains(t, defaultConfig, "{{OUTPUT_FORMATS}}") + + guide, err := os.ReadFile(filepath.Join("..", "docs", "guides", "configuration.md")) + require.NoError(t, err) + assert.Contains(t, string(guide), "# Valid values: "+meta.OutputFormatList) + assert.Contains(t, string(guide), "| `format` | string | `json` | `"+strings.Join(meta.OutputFormatNames(), "`, `")+"` |") + assert.Contains(t, string(guide), "| `output.format` | `"+strings.Join(meta.OutputFormatNames(), "`, `")+"` |") +} + +func readGolden(t *testing.T, name string) string { + t.Helper() + b, err := os.ReadFile(filepath.Join("testdata", name)) + require.NoError(t, err) + return string(b) } func TestScanCommand_NoSubcommand_ShowsHelp(t *testing.T) { diff --git a/cmd/scan_config_test.go b/cmd/scan_config_test.go index 9430f0d..db32580 100644 --- a/cmd/scan_config_test.go +++ b/cmd/scan_config_test.go @@ -1,6 +1,7 @@ package cmd import ( + "context" "os" "path/filepath" "runtime" @@ -9,6 +10,12 @@ import ( "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/internal/detector/testutil" + "github.com/HodeTech/leakwatch/internal/scanner" + "github.com/HodeTech/leakwatch/internal/verifier" + "github.com/HodeTech/leakwatch/pkg/finding" ) // newTestScanCmd builds a cobra command carrying the same common scan flags that @@ -242,6 +249,179 @@ func TestLoadScanConfig_IsolatedPerCommand_NoCrossLeak(t *testing.T) { assert.Equal(t, runtime.NumCPU(), cfgB.Concurrency) } +func TestLoadScanConfig_GrafanaInstanceRequiresExplicitFlag(t *testing.T) { + clearScanEnv(t) + writeConfigFile(t, "verification:\n grafana-instance-url: https://repository-controlled.example\n") + t.Setenv("LEAKWATCH_VERIFICATION_GRAFANA_INSTANCE_URL", "https://environment.example") + + cmd := newTestScanCmd() + require.NoError(t, cmd.ParseFlags(nil)) + cfg, err := loadScanConfig(cmd) + require.NoError(t, err) + assert.Empty(t, cfg.GrafanaInstanceURL, "project config and environment must not choose a verifier target") + + cmd = newTestScanCmd() + require.NoError(t, cmd.ParseFlags([]string{"--grafana-instance-url", "https://operator.example"})) + cfg, err = loadScanConfig(cmd) + require.NoError(t, err) + assert.Equal(t, "https://operator.example", cfg.GrafanaInstanceURL) + + registered, ok := verifier.Get("grafana-api-key") + require.True(t, ok) + engineCfg, err := scanner.BuildEngineConfig(cfg) + require.NoError(t, err) + var configured verifier.Verifier + for _, candidate := range engineCfg.Verifiers { + if candidate.Type() == "grafana-api-key" { + configured = candidate + break + } + } + require.NotNil(t, configured) + assert.NotSame(t, registered, configured, "the scan must receive a configured per-run verifier copy") + stillRegistered, ok := verifier.Get("grafana-api-key") + require.True(t, ok) + assert.Same(t, registered, stillRegistered, "per-run configuration must not mutate the global registry") + + for _, rawURL := range []string{"http://grafana.example", "https://127.0.0.1", "https://grafana.com"} { + cfg.GrafanaInstanceURL = rawURL + _, err = scanner.BuildEngineConfig(cfg) + require.Error(t, err) + assert.Contains(t, err.Error(), `configure trusted verification for "grafana-api-key"`) + } +} + +func TestLoadScanConfig_TrustedVerifierOriginsRequireExplicitFlags(t *testing.T) { + clearScanEnv(t) + writeConfigFile(t, "verification:\n verifier-origin:\n gitlab-pat: https://repository-controlled.example\n") + t.Setenv("LEAKWATCH_VERIFICATION_VERIFIER_ORIGIN", "gitlab-pat=https://environment.example") + + cmd := newTestScanCmd() + require.NoError(t, cmd.ParseFlags(nil)) + cfg, err := loadScanConfig(cmd) + require.NoError(t, err) + assert.Empty(t, cfg.TrustedVerifierOrigins, + "project config and environment must not choose credential destinations") + + cmd = newTestScanCmd() + require.NoError(t, cmd.ParseFlags([]string{ + "--verifier-origin", "gitlab-pat=https://gitlab.operator.example", + "--verifier-origin", "auth0-management-token=https://tenant.operator.example", + })) + cfg, err = loadScanConfig(cmd) + require.NoError(t, err) + assert.Equal(t, map[string]string{ + "gitlab-pat": "https://gitlab.operator.example", + "auth0-management-token": "https://tenant.operator.example", + }, cfg.TrustedVerifierOrigins) +} + +func TestLoadScanConfig_TrustedVerifierOriginSyntaxFailsClosed(t *testing.T) { + for _, args := range [][]string{ + {"--verifier-origin", "missing-equals"}, + {"--verifier-origin", "=https://operator.example"}, + {"--verifier-origin", "gitlab-pat="}, + { + "--verifier-origin", "gitlab-pat=https://one.example", + "--verifier-origin", "gitlab-pat=https://two.example", + }, + } { + cmd := newTestScanCmd() + require.NoError(t, cmd.ParseFlags(args)) + _, err := loadScanConfig(cmd) + require.Error(t, err, args) + assert.Contains(t, err.Error(), "--verifier-origin") + } +} + +func TestBuildEngineConfig_ConfiguresEveryContextRequiredVerifierPerRun(t *testing.T) { + origins := map[string]string{ + "auth0-management-token": "https://tenant.example", + "datadog-api-key": "https://api.datadoghq.com", + "github-token": "https://github.example", + "github-oauth-token": "https://github.example", + "gitlab-pat": "https://gitlab.example", + "grafana-api-key": "https://grafana.example", + "shopify-access-token": "https://store.myshopify.com", + "snyk-api-key": "https://api.snyk.io", + "twilio-api-key": "https://api.twilio.com", + } + originals := make(map[string]verifier.Verifier, len(origins)) + for detectorID := range origins { + registered, ok := verifier.Get(detectorID) + require.True(t, ok, detectorID) + originals[detectorID] = registered + } + + engineCfg, err := scanner.BuildEngineConfig(&scanner.Config{ + Concurrency: 1, + TrustedVerifierOrigins: origins, + }) + require.NoError(t, err) + configured := make(map[string]verifier.Verifier, len(engineCfg.Verifiers)) + for _, candidate := range engineCfg.Verifiers { + configured[candidate.Type()] = candidate + } + for detectorID, original := range originals { + require.Contains(t, configured, detectorID) + assert.NotSame(t, original, configured[detectorID], detectorID) + stillRegistered, ok := verifier.Get(detectorID) + require.True(t, ok) + assert.Same(t, original, stillRegistered, detectorID) + } + + // Drive every configured production verifier with the exact finding emitted + // by its registered detector. An already-cancelled context guarantees that + // this executable routing contract cannot put synthetic credentials on the + // network while proving the per-run configured path is reached. + detectors := make(map[string]detector.Detector, len(detectorsAtInit)) + for _, det := range detectorsAtInit { + detectors[det.ID()] = det + } + fixtures := testutil.RegisteredDetectorFixtures() + ctx, cancel := context.WithCancel(t.Context()) + cancel() + for detectorID := range origins { + fixture, ok := fixtures[detectorID] + require.True(t, ok, detectorID) + rawFindings := testutil.ScanViaMatcher(detectors[detectorID], fixture.Input) + require.NotEmpty(t, rawFindings, detectorID) + result := configured[detectorID].Verify(ctx, rawFindings[0]) + assert.Equal(t, finding.StatusVerifyError, result.Status, + "configured verifier must reach its cancelled provider request: %s (%s)", detectorID, result.Message) + assert.NotContains(t, result.Message, string(rawFindings[0].Raw)) + } +} + +func TestBuildEngineConfig_RejectsInvalidTrustedVerifierOrigins(t *testing.T) { + tests := []struct { + name string + origins map[string]string + }{ + {name: "unknown detector", origins: map[string]string{"missing": "https://operator.example"}}, + {name: "non contextual verifier", origins: map[string]string{"aws-access-key-id": "https://operator.example"}}, + {name: "invalid origin", origins: map[string]string{"gitlab-pat": "http://gitlab.example"}}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + _, err := scanner.BuildEngineConfig(&scanner.Config{ + Concurrency: 1, + TrustedVerifierOrigins: tc.origins, + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "configure trusted verification") + }) + } + + _, err := scanner.BuildEngineConfig(&scanner.Config{ + Concurrency: 1, + GrafanaInstanceURL: "https://grafana-one.example", + TrustedVerifierOrigins: map[string]string{"grafana-api-key": "https://grafana-two.example"}, + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "conflicting Grafana origins") +} + func TestShouldEnableColor_DecisionTable(t *testing.T) { tests := []struct { name string diff --git a/cmd/scan_fs_multiarg_test.go b/cmd/scan_fs_multiarg_test.go index 2259bd0..5a07196 100644 --- a/cmd/scan_fs_multiarg_test.go +++ b/cmd/scan_fs_multiarg_test.go @@ -24,16 +24,13 @@ const syntheticAWSKey = "AKIAZ7XKJQR4TMPLW2NF" func scanFsJSONFindings(t *testing.T, extraArgs ...string) []map[string]any { t.Helper() - prevCfg := cfgFile - cfgFile = filepath.Join(t.TempDir(), "absent.yaml") - t.Cleanup(func() { cfgFile = prevCfg }) + isolateConfig(t) outFile := filepath.Join(t.TempDir(), "out.json") args := append([]string{"scan", "fs"}, extraArgs...) args = append(args, "--no-verify", "--format", "json", "--output", outFile) - rootCmd.SetArgs(args) - err := rootCmd.Execute() + err := executeRoot(t, args...) // A findings-present run returns FindingsExitError; that is expected here and // is not a failure of the test harness. if err != nil { diff --git a/cmd/scan_gcs.go b/cmd/scan_gcs.go index 96aaf9d..12095ee 100644 --- a/cmd/scan_gcs.go +++ b/cmd/scan_gcs.go @@ -61,5 +61,5 @@ func runScanGCS(cmd *cobra.Command, args []string) error { cfg.ScanTarget = "gs://" + args[0] src := gcssource.New(args[0], opts...) - return runScan(cmd, cfg, src, nil) + return runScan(cmd, cfg, src, src) } diff --git a/cmd/scan_rune_test.go b/cmd/scan_rune_test.go index 2efce4b..bc46ec8 100644 --- a/cmd/scan_rune_test.go +++ b/cmd/scan_rune_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "errors" + "fmt" "os" "path/filepath" "testing" @@ -15,9 +16,43 @@ import ( "github.com/HodeTech/leakwatch/internal/engine" "github.com/HodeTech/leakwatch/internal/scanner" + "github.com/HodeTech/leakwatch/internal/source" "github.com/HodeTech/leakwatch/pkg/finding" ) +type cancelDuringValidationSource struct { + cancel context.CancelFunc +} + +type emptyTestSource struct{} + +func (*emptyTestSource) Type() string { return "empty-test" } +func (*emptyTestSource) Err() error { return nil } +func (*emptyTestSource) Validate(context.Context) error { return nil } +func (*emptyTestSource) Chunks(context.Context) <-chan source.Chunk { + ch := make(chan source.Chunk) + close(ch) + return ch +} + +type failingCloser struct{ err error } + +func (c *failingCloser) Close() error { return c.err } + +func (*cancelDuringValidationSource) Type() string { return "validation-cancel" } +func (*cancelDuringValidationSource) Err() error { return nil } +func (s *cancelDuringValidationSource) Validate(ctx context.Context) error { + s.cancel() + <-ctx.Done() + return ctx.Err() +} + +func (*cancelDuringValidationSource) Chunks(context.Context) <-chan source.Chunk { + ch := make(chan source.Chunk) + close(ch) + return ch +} + // executeRoot runs the real root command with the given args, capturing cobra's // own output so it does not leak into the test log, and returns the RunE error. // @@ -160,6 +195,28 @@ func TestRunScanSlack_TokenFromEnv_ReachesDateValidation(t *testing.T) { assert.Contains(t, err.Error(), "invalid --since date") } +func TestScanSlackHelp_DocumentsRequiredOAuthScopes(t *testing.T) { + for _, scope := range []string{ + "channels:read", "channels:history", "groups:read", "groups:history", + "im:read", "im:history", "mpim:read", "mpim:history", "files:read", + } { + assert.Contains(t, scanSlackCmd.Long, scope) + } + rateFlag := scanSlackCmd.Flags().Lookup("rate-limit") + require.NotNil(t, rateFlag) + assert.Contains(t, rateFlag.Usage, "per-operation") + assert.Equal(t, "0", rateFlag.DefValue) +} + +func TestRunScanSlack_RejectsNegativeRateLimitBeforeNetwork(t *testing.T) { + isolateConfig(t) + t.Setenv("LEAKWATCH_SLACK_TOKEN", "xoxb-fake-test-token") + + err := executeRoot(t, "scan", "slack", "--rate-limit", "-1") + require.Error(t, err) + assert.Contains(t, err.Error(), "--rate-limit must be zero or greater") +} + func TestRunScanRepos_AllReposFail_ReturnsAggregateError(t *testing.T) { isolateConfig(t) // Two local paths that are not git repositories fail to open locally (no @@ -201,6 +258,18 @@ func TestFinishScan_ExitCodeContract(t *testing.T) { err := finishScan(base, result, "fs", context.Canceled) var fErr *FindingsExitError require.ErrorAs(t, err, &fErr) + assert.ErrorIs(t, err, context.Canceled) + assert.Contains(t, err.Error(), "partial results") + }) + + t.Run("findings preserve a non-interruption source error", func(t *testing.T) { + scanErr := errors.New("source pagination failed") + result := &engine.ScanResult{Findings: []finding.Finding{{ID: "x"}}} + err := finishScan(base, result, "slack", scanErr) + var findingsErr *FindingsExitError + require.ErrorAs(t, err, &findingsErr) + assert.ErrorIs(t, err, scanErr) + assert.Contains(t, err.Error(), "partial results") }) t.Run("clean completed scan returns nil", func(t *testing.T) { @@ -209,6 +278,58 @@ func TestFinishScan_ExitCodeContract(t *testing.T) { }) } +func TestRunScan_CancelledValidationReturnsInterruptedExit(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cmd := &cobra.Command{} + cmd.SetContext(ctx) + src := &cancelDuringValidationSource{cancel: cancel} + cfg := &scanner.Config{ + Concurrency: 1, + NoVerify: true, + Format: "json", + OutputFile: filepath.Join(t.TempDir(), "result.json"), + } + + err := runScan(cmd, cfg, src, nil) + var interrupted *InterruptedExitError + require.ErrorAs(t, err, &interrupted) + assert.Equal(t, 3, exitCodeForError(err), "cancelled validation must reach the process exit-3 contract") +} + +func TestRunScan_CleanupFailureCannotReturnSuccess(t *testing.T) { + cleanupErr := errors.New("synthetic cleanup failure") + cfg := &scanner.Config{ + Concurrency: 1, + NoVerify: true, + Format: "json", + OutputFile: filepath.Join(t.TempDir(), "result.json"), + } + cmd := &cobra.Command{} + cmd.SetContext(context.Background()) + + err := runScan(cmd, cfg, &emptyTestSource{}, &failingCloser{err: cleanupErr}) + require.ErrorIs(t, err, cleanupErr) + assert.Equal(t, 2, exitCodeForError(err)) +} + +func TestExitCodeForError(t *testing.T) { + tests := []struct { + name string + err error + want int + }{ + {name: "clean", want: 0}, + {name: "findings", err: fmt.Errorf("wrapped: %w", &FindingsExitError{Count: 2}), want: 1}, + {name: "generic failure", err: errors.New("boom"), want: 2}, + {name: "interrupted", err: fmt.Errorf("wrapped: %w", &InterruptedExitError{}), want: 3}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.want, exitCodeForError(tc.err)) + }) + } +} + func TestLoadScanConfig_InvalidMinSeverity_ReturnsError(t *testing.T) { isolateConfig(t) cmd := newTestScanCmd() diff --git a/cmd/scan_slack.go b/cmd/scan_slack.go index c35bd30..1e9ee97 100644 --- a/cmd/scan_slack.go +++ b/cmd/scan_slack.go @@ -2,7 +2,6 @@ package cmd import ( "fmt" - "log/slog" "os" "strings" "time" @@ -13,27 +12,27 @@ import ( ) // flagIncludeFiles is the slack-only flag that requests scanning of uploaded -// file content. Defined as a constant so the registration, deprecation, and -// read sites reference a single literal. +// file content. Defined as a constant so registration and read sites reference +// a single literal. const flagIncludeFiles = "include-files" -// defaultSlackRateLimit is the default for the --rate-limit flag. It mirrors the -// slack source's own conservative defaultRateLimit (1 req/s): because the flag -// is always applied when > 0, a mismatched flag default would silently override -// the source's default and defeat it. Keep this in sync with -// internal/source/slack defaultRateLimit until that constant is exported. -const defaultSlackRateLimit = 1.0 +// A zero CLI value retains the source package's safe operation-specific +// defaults. A positive value explicitly overrides every limiter bucket. +const defaultSlackRateLimit = 0 var scanSlackCmd = &cobra.Command{ Use: "slack", Short: "Scans a Slack workspace", Long: `Scans messages across channels in a Slack workspace to detect leaked secrets -such as API keys, passwords, and certificates. Scanning of uploaded file -content is planned but not yet implemented; only message text is scanned. - -Requires a Slack Bot Token with appropriate scopes (channels:history, -groups:history, im:history, mpim:history). The token can be provided via the ---token flag or the LEAKWATCH_SLACK_TOKEN environment variable.`, +such as API keys, passwords, and certificates. Text-like uploaded files can be +scanned with --include-files; downloads are size-bounded and restricted to +Slack-owned HTTPS endpoints. + +Requires a Slack Bot Token with channels:read, channels:history, groups:read, +and groups:history. --include-dms additionally requires im:read, im:history, +mpim:read, and mpim:history; file scanning additionally requires files:read. +The token can be provided via the --token flag or the LEAKWATCH_SLACK_TOKEN +environment variable.`, Example: ` # Scan all channels using environment variable for token export LEAKWATCH_SLACK_TOKEN=xoxb-your-token leakwatch scan slack @@ -50,8 +49,11 @@ groups:history, im:history, mpim:history). The token can be provided via the # Include direct messages leakwatch scan slack --include-dms - # Reduce API rate to avoid throttling - leakwatch scan slack --rate-limit 10`, + # Include text-like file attachments (requires files:read) + leakwatch scan slack --include-files --max-file-size 5242880 + + # Use a higher rate for a Marketplace/internal app with Tier 3 history access + leakwatch scan slack --rate-limit 0.8`, Args: cobra.NoArgs, RunE: runScanSlack, } @@ -65,14 +67,8 @@ func init() { flags.String("exclude-channels", "", "comma-separated channel names to exclude") flags.String("since", "", "scan messages after this date (YYYY-MM-DD)") flags.Bool("include-dms", false, "include direct messages") - // Slack file scanning is not yet implemented (only message text is scanned). - // The flag is kept for forward-compatibility but defaults to false, is hidden, - // and is marked deprecated so the CLI does not advertise a working feature. - flags.Bool(flagIncludeFiles, false, "(not yet implemented; planned) scan uploaded file content") - if err := flags.MarkDeprecated(flagIncludeFiles, "Slack file scanning is not yet implemented; this flag has no effect"); err != nil { - slog.Warn("failed to mark include-files deprecated", "error", err) - } - flags.Float64("rate-limit", defaultSlackRateLimit, "max Slack API requests per second") + flags.Bool(flagIncludeFiles, false, "scan text-like uploaded files (requires files:read)") + flags.Float64("rate-limit", defaultSlackRateLimit, "per-operation Slack request cap per second (0 uses safe operation-specific defaults)") addCommonScanFlags(flags) addVerifyFlags(flags) } @@ -92,7 +88,7 @@ func runScanSlack(cmd *cobra.Command, _ []string) error { return fmt.Errorf("slack bot token is required: use --token or set LEAKWATCH_SLACK_TOKEN") } - var opts []slacksource.Option + opts := []slacksource.Option{slacksource.WithMaxFileSize(cfg.MaxFileSize)} if channels := flagString(cmd, "channels"); channels != "" { opts = append(opts, slacksource.WithChannels(splitComma(channels))) @@ -118,7 +114,11 @@ func runScanSlack(cmd *cobra.Command, _ []string) error { opts = append(opts, slacksource.WithIncludeFiles(true)) } - if rateLimit := flagFloat64(cmd, "rate-limit"); rateLimit > 0 { + rateLimit := flagFloat64(cmd, "rate-limit") + if rateLimit < 0 { + return fmt.Errorf("--rate-limit must be zero or greater") + } + if rateLimit > 0 { opts = append(opts, slacksource.WithRateLimit(rateLimit)) } diff --git a/cmd/stats_test.go b/cmd/stats_test.go index 993bb2b..0a78551 100644 --- a/cmd/stats_test.go +++ b/cmd/stats_test.go @@ -1,12 +1,18 @@ package cmd import ( + "context" + "sort" + "strings" "testing" "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/internal/detector/testutil" "github.com/HodeTech/leakwatch/internal/meta" "github.com/HodeTech/leakwatch/internal/verifier" + "github.com/HodeTech/leakwatch/pkg/finding" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // detectorsAtInit and verifiersAtInit snapshot the registries right after every @@ -23,6 +29,124 @@ func init() { verifiersAtInit = verifier.All() } +// TestVerifierContracts_UseRealDetectorOutput proves registry compatibility +// with findings emitted by the production detectors, not hand-assembled +// RawFinding values. It executes each verifier with an already-cancelled +// context: this proves the real finding reaches the declared contract while +// preventing synthetic credentials from reaching provider networks. +func TestVerifierContracts_UseRealDetectorOutput(t *testing.T) { + fixtures := testutil.RegisteredDetectorFixtures() + detectors := make(map[string]detector.Detector, len(detectorsAtInit)) + for _, det := range detectorsAtInit { + detectors[det.ID()] = det + } + capabilities := make(map[string]meta.VerificationCapability, len(detectorsAtInit)) + for _, capability := range meta.VerificationCapabilities() { + capabilities[capability.DetectorID] = capability + } + + for _, registered := range verifiersAtInit { + id := registered.Type() + t.Run(id, func(t *testing.T) { + capability, ok := capabilities[id] + require.True(t, ok, "verifier has no capability metadata") + assert.NotEqual(t, meta.VerifierNone, capability.VerifierKind, + "registered verifier cannot be categorized as none") + + det, ok := detectors[id] + require.True(t, ok, "verifier has no registered production detector") + fixture, ok := fixtures[id] + require.True(t, ok, "verifier detector has no shared contract fixture") + findings := testutil.ScanViaMatcher(det, fixture.Input) + require.NotEmpty(t, findings, "real matcher/detector path produced no verifier input") + assert.Equal(t, id, findings[0].DetectorID) + + for _, field := range capability.RequiredContextFields { + if isOperatorSuppliedContext(field) { + continue + } + if field == "raw_v2" { + assert.NotEmpty(t, findings[0].RawV2, + "detector must emit required companion field %q", field) + continue + } + assert.NotEmpty(t, findings[0].ExtraData[field], + "detector must emit required companion field %q", field) + } + + // Execute the production verifier with an already-cancelled context. + // Standard HTTP clients and SDKs cannot put the synthetic credential on + // the wire, while the resulting status proves whether the real detector + // output actually reached the capability's declared verification path. + ctx, cancel := context.WithCancel(t.Context()) + cancel() + result := registered.Verify(ctx, findings[0]) + assert.NotContains(t, result.Message, string(findings[0].Raw)) + for key, value := range result.ExtraData { + assert.NotContains(t, value, string(findings[0].Raw), "ExtraData[%q] leaked fixture credential", key) + } + switch capability.VerifierKind { + case meta.VerifierLive: + assert.Equal(t, finding.StatusVerifyError, result.Status, + "live capability must reach its context-cancelled provider request") + message := strings.ToLower(result.Message) + assert.True(t, strings.Contains(message, "context canceled") || strings.Contains(message, "cancelled"), + "live capability must fail at the cancelled provider boundary, not at an unrelated early return: %s", result.Message) + case meta.VerifierFormatOnly: + assert.Equal(t, finding.StatusUnverified, result.Status) + assert.True(t, strings.Contains(strings.ToLower(result.Message), "format valid"), + "format-only fixture must satisfy the production format contract: %s", result.Message) + case meta.VerifierRequiresContext: + assert.Equal(t, finding.StatusUnverified, result.Status, + "missing operator context must fail closed without a request") + } + }) + } +} + +func isOperatorSuppliedContext(field string) bool { + switch field { + case "trusted_api_origin", "trusted_instance_origin", "trusted_store_origin": + return true + default: + return false + } +} + +// TestCapabilityManifest_MatchesRuntimeRegistries prevents registry count from +// being confused with product capability. Every detector must have exactly one +// manifest entry, and only live/format/context-required entries may have a +// registered verifier. +func TestCapabilityManifest_MatchesRuntimeRegistries(t *testing.T) { + capabilities := meta.VerificationCapabilities() + manifestDetectorIDs := make([]string, 0, len(capabilities)) + manifestVerifierIDs := make([]string, 0, meta.Verifiers) + for _, capability := range capabilities { + manifestDetectorIDs = append(manifestDetectorIDs, capability.DetectorID) + if capability.VerifierKind != meta.VerifierNone { + manifestVerifierIDs = append(manifestVerifierIDs, capability.DetectorID) + } + } + sort.Strings(manifestDetectorIDs) + sort.Strings(manifestVerifierIDs) + + runtimeDetectorIDs := make([]string, 0, len(detectorsAtInit)) + for _, registered := range detectorsAtInit { + runtimeDetectorIDs = append(runtimeDetectorIDs, registered.ID()) + } + runtimeVerifierIDs := make([]string, 0, len(verifiersAtInit)) + for _, registered := range verifiersAtInit { + runtimeVerifierIDs = append(runtimeVerifierIDs, registered.Type()) + } + sort.Strings(runtimeDetectorIDs) + sort.Strings(runtimeVerifierIDs) + + assert.Equal(t, runtimeDetectorIDs, manifestDetectorIDs, + "capability manifest must classify every registered detector exactly once") + assert.Equal(t, runtimeVerifierIDs, manifestVerifierIDs, + "registered verifier IDs must equal all non-none capability entries") +} + // TestMetaCounts_MatchRuntime guards the published counts in internal/meta // against what the binary actually registers. Every detector and verifier // package is blank-imported by imports.go in this package, so both registries diff --git a/cmd/testdata/output-formats.golden b/cmd/testdata/output-formats.golden new file mode 100644 index 0000000..6bf392b --- /dev/null +++ b/cmd/testdata/output-formats.golden @@ -0,0 +1,5 @@ +json +sarif +csv +table +github diff --git a/cmd/testdata/root-help.golden b/cmd/testdata/root-help.golden new file mode 100644 index 0000000..545609b --- /dev/null +++ b/cmd/testdata/root-help.golden @@ -0,0 +1,46 @@ +Leakwatch is a high-performance security tool that detects, verifies, and reports +leaked secrets (API keys, passwords, certificates) in codebases, Git histories, +container images, cloud storage buckets, and Slack workspaces. + +Features: + - 65 built-in secret detectors covering AWS, GitHub, Slack, Stripe, JWT, and more + - 54 verification implementations: 39 direct-live, 9 context-required, 6 format-only + - Scans filesystems, Git repos, container images, S3, GCS, and Slack + - Multiple output formats: json, sarif, csv, table, github + - Aho-Corasick pre-filtering for fast multi-pattern matching + - Concurrent worker pool architecture for high throughput + - Custom rules via YAML configuration + - .leakwatchignore and inline ignore support + +Usage: + leakwatch [command] + +Examples: + # Quick scan of current directory + leakwatch scan fs . + + # Scan a Git repository with verification + leakwatch scan git https://github.com/org/repo.git + + # Scan and output SARIF for GitHub Code Scanning + leakwatch scan fs . --format sarif --output results.sarif + + # Scan with remediation guidance + leakwatch scan fs . --remediation --format table + + # Show only verified active secrets + leakwatch scan git . --only-verified + +Available Commands: + completion Generate the autocompletion script for the specified shell + help Help about any command + init Creates a .leakwatch.yaml configuration file + scan Starts a secret scan + version Shows Leakwatch version information + +Flags: + --config string config file (default: .leakwatch.yaml) + -h, --help help for leakwatch + --log-level string log level (debug, info, warn, error) (default "warn") + +Use "leakwatch [command] --help" for more information about a command. diff --git a/cmd/testdata/version.golden b/cmd/testdata/version.golden new file mode 100644 index 0000000..e066974 --- /dev/null +++ b/cmd/testdata/version.golden @@ -0,0 +1 @@ +leakwatch 1.0.0-test (commit: abc1234, built: 2026-03-24) diff --git a/cmd/version.go b/cmd/version.go index a71a0d4..81ee332 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -1,16 +1,24 @@ package cmd -import "github.com/spf13/cobra" +import ( + "fmt" + + "github.com/spf13/cobra" +) var versionCmd = &cobra.Command{ Use: "version", Short: "Shows Leakwatch version information", Example: ` leakwatch version`, Run: func(cmd *cobra.Command, args []string) { - cmd.Printf("leakwatch %s (commit: %s, built: %s)\n", buildVersion, buildCommit, buildDate) + cmd.Print(formatVersion(buildVersion, buildCommit, buildDate)) }, } +func formatVersion(version, commit, date string) string { + return fmt.Sprintf("leakwatch %s (commit: %s, built: %s)\n", version, commit, date) +} + func init() { rootCmd.AddCommand(versionCmd) } diff --git a/docs/05-ROADMAP.md b/docs/05-ROADMAP.md index 12df8d2..b69a485 100644 --- a/docs/05-ROADMAP.md +++ b/docs/05-ROADMAP.md @@ -1,9 +1,9 @@ # Leakwatch - Phased Development Roadmap -> **Document Version:** 7.1 +> **Document Version:** 7.2 > **Date:** 2026-04-09 > **Status:** Approved -> **Last Updated:** 2026-05-25 +> **Last Updated:** 2026-08-11 --- @@ -24,14 +24,46 @@ | Phase 8.3 — Scan Summary + Security | Completed | `v1.4.0` | 2026-04-08 | | Phase 8.4 — False Positive Reduction | Completed | `v1.5.0` | 2026-04-09 | | Phase 8.5 — GitHub Marketplace Action & Distribution | Completed | `v1.6.0` | 2026-05-25 | -| Phase 9 — Detection Accuracy & FP Reduction | Planned | `v1.7.0` | — | -| Phase 10 — Detector Library Expansion | Planned | `v1.8.0` | — | -| Phase 11 — Verification Depth & Credential Impact | Planned | `v1.9.0` | — | -| Phase 12 — Source Expansion (Confluence/Jira, org-scale) | Planned | `v1.10.0` | — | -| Phase 13 — Secrets Inventory | Planned | `v1.11.0` | — | -| Phase 14 — Honeytokens | Planned | `v1.12.0` | — | +| Review Remediation Release | Released | `v1.7.0` | 2026-07-20 | +| Phase 9 — Detection Accuracy & FP Reduction | Completed | `v1.8.0` | 2026-08-11 | +| Phase 10 — Detector Library Expansion | Planned | `v1.9.0` | — | +| Phase 11 — Verification Depth & Credential Impact | Planned | `v1.10.0` | — | +| Phase 12 — Source Expansion (Confluence/Jira, org-scale) | Planned | `v1.11.0` | — | +| Phase 13 — Secrets Inventory | Planned | `v1.12.0` | — | +| Phase 14 — Honeytokens | Planned | `v1.13.0` | — | -> **Prioritization note (v7.0):** the planned sequence is re-ordered so the work that most strengthens the core promise — accurate, verified, low-noise findings — comes first. Detection accuracy and false-positive reduction (Phase 9), broader coverage of high-blast-radius credential types (Phase 10), and deeper verification with credential-impact insight (Phase 11) precede new scan sources (Phase 12) and the inventory/honeytoken platform features (Phases 13–14). Rationale is detailed in [Planned Work — Prioritization](#planned-work--prioritization). +> **Prioritization note (v7.0):** the planned sequence is re-ordered so the work that most strengthens the core promise — accurate, verified, low-noise findings — comes first. Detection accuracy and false-positive reduction (Phase 9, completed in `v1.8.0`), broader coverage of high-blast-radius credential types (Phase 10), and deeper verification with credential-impact insight (Phase 11) precede new scan sources (Phase 12) and the inventory/honeytoken platform features (Phases 13–14). Rationale is detailed in [Planned Work — Prioritization](#planned-work--prioritization). + +### v1.8.0 Highlights + +- **Detection accuracy** — structured configuration and generic API-key detection now cover realistic SDK, shell, YAML, JSON, TOML, INI, dotenv, and XML shapes while rejecting source-code, prose, locale, placeholder, and package-version lookalikes +- **Verification reliability** — inactive credentials require strict provider-authentication evidence and JSON response contracts; trusted origins, regional fallbacks, rate admission, cancellation, panic containment, and metadata enrichment fail conservatively +- **Source resilience** — Slack pagination, attachment failures, rate-limit waits, Git cancellation/cleanup, and GCS client ownership now surface partial failures without silent clean scans or lifecycle races +- **Truthful product surface** — capability counts, verifier context requirements, CLI flags, EN/TR manuals, generated site content, release metadata, and CI drift guards share machine-checked sources of truth +- **Security and quality gates** — mutation-sensitive HTTP/SSRF/rate-limit/display-width tests, full race coverage, provider-contract audits, protected VSIX publication, and release-time registry checks defend the published artifact + +### v1.7.0 Highlights + +- **Full-project remediation release** — findings from a multi-dimensional review of the code, tests, documentation, CI/CD, VS Code extension, and website were remediated and recorded in the changelog +- **Detection and scan correctness** — expanded token coverage, per-file scanning, consistent exclusion flags, deterministic and bounded engine behavior, correct Git-history attribution, and stricter false-positive handling +- **Verification accuracy** — corrected provider endpoint/context handling and removed verification paths that could not truthfully prove credential liveness +- **Secret-safe output and transport** — raw credentials are excluded from default output and logs; panic, URL, terminal, CSV, container, and editor trust boundaries were hardened +- **Quality and supply-chain gates** — detector coverage, website drift, VS Code, Windows build, SBOM/signing, security scanning, and dependency/toolchain checks were strengthened + +### Post-v1.7.0 hardening-wave status + +| Product capability | Status | Delivery boundary | +|---|---|---| +| Slack text-file attachments | **Shipped** | Explicit `--include-files`, bounded text-only downloads from Slack-owned HTTPS endpoints | +| GitHub/GitLab scope and expiry metadata | **Shipped** | Best-effort metadata after authoritative active verification | +| VSIX build and Marketplace icon | **Shipped** | Lockfile-pinned CI package and reviewable artifact | +| Visual Studio Marketplace publication | **Experimental** | Manual, protected-environment promotion only | +| Provider contract freshness audit | **Shipped** | Primary references plus weekly 180-day gate | +| Baseline/snapshot suppression | **Planned** | HMAC-keyed design and threat model in ADR-0011; no current CLI surface | +| Coinbase legacy live HMAC verification | **Planned** | Format-only until unambiguous pair correlation and safe provider proof exist | +| Live provider canaries | **Planned** | Provider-specific, secret-safe protected opt-in; never default CI | + +See [ADR-0011](decisions/ADR-0011-product-trust-boundaries.md) for the trust boundaries and acceptance requirements behind these labels. ### v1.6.0 Highlights @@ -94,7 +126,7 @@ ## Roadmap Overview -Leakwatch development proceeds in incremental phases, each building on the previous one and each producing a usable deliverable. Phases 1–8 (through `v1.6.0`) are complete; Phases 9–14 are the planned forward path, sequenced by leverage on the product's core promise — see [Planned Work — Prioritization](#planned-work--prioritization). +Leakwatch development proceeds in incremental phases, each building on the previous one and each producing a usable deliverable. Phases 1–9 (through `v1.8.0`) are complete. Phases 10–14 remain the planned forward path beginning with `v1.9.0`, sequenced by leverage on the product's core promise — see [Planned Work — Prioritization](#planned-work--prioritization). ```mermaid gantt @@ -134,8 +166,11 @@ gantt UX, Security, FP reduction :done, f8, after f6, 6w Marketplace Action & distrib. :done, f85, after f8, 3w - section Planned v1.7.0+ - Detection accuracy & FP :p9, after f85, 5w + section Completed v1.7.0-v1.8.0 + Full-project review remediation :done, f87, after f85, 4w + Detection accuracy & FP :done, p9, after f87, 5w + + section Planned v1.9.0+ Detector library expansion :p10, after p9, 6w Verification depth & impact :p11, after p10, 6w Source expansion :p12, after p11, 6w @@ -422,36 +457,36 @@ The product's core promise is **accurate, verified, low-noise secret findings**. **Goal:** Make accuracy a measurable strength. Raise detector precision and recall, cut false positives across the board, and ensure every documented detection/verification behavior actually fires. This phase improves the quality of every existing scan without adding new surfaces. -**Duration:** 4-5 weeks | **Version:** `v1.7.0` | **Status:** Planned +**Duration:** 4-5 weeks | **Version:** `v1.8.0` | **Status:** Planned ### Deliverables -| Task | Priority | Description | -|------|----------|-------------| -| Centralized false-positive filter | Critical | Shared module applied before verification: common placeholder values, a dictionary/word-list of dummy strings, and known non-secret patterns, so individual detectors no longer each re-implement ad-hoc skips | -| Engine-level entropy gating | Critical | Apply the configured `detection.entropy.threshold` in the detection engine itself (today only custom YAML rules honor it), so low-entropy matches are dropped consistently | -| Keyword pre-filters for high-noise detectors | High | Add Aho-Corasick pre-filter keywords / context anchors to detectors that currently scan every chunk (e.g. Telegram, Discord), which today fire on any token-shaped string | -| Broaden OpenAI key coverage | High | Detect legacy and service-account key variants in addition to project keys, anchoring on the embedded provider marker for precision (fewer misses, fewer false positives) | -| GitHub fine-grained PAT support | High | Extend the GitHub detector to cover fine-grained personal access tokens (`github_pat_`), now a default token type | -| Wire okta / shopify / bitbucket verification | High | Populate the `ExtraData` fields (`domain`, `store_domain`, `username`) these verifiers require, so the three detectors actually reach verified/inactive instead of always-unverified | -| Tighten Anthropic key regex | Medium | Anchor exact length and trailing marker; distinguish admin from standard keys | -| Mailgun & JWT format breadth | Medium | Add the alternate Mailgun key format; broaden JWT matching to pretty-printed / base64-variant headers and optional padding | -| Supabase service-role detection | Medium | Detect service-role JWTs (`role: service_role`) in addition to management PATs, so coverage matches the detector's name | -| Bounded / streaming result buffering | Medium | Cap or stream the in-memory finding buffer so adversarially large inputs cannot exhaust memory before the verification phase | -| Accuracy benchmark suite | High | Curated true/false corpus measuring precision and recall per detector, run in CI to guard against regressions | +| Task | Status | Priority | Description | +|------|--------|----------|-------------| +| Centralized false-positive filter | Planned | Critical | Shared module applied before verification: common placeholder values, a dictionary/word-list of dummy strings, and known non-secret patterns, so individual detectors no longer each re-implement ad-hoc skips | +| Engine-level entropy gating | Delivered in `v1.7.0` | Critical | The configured `detection.entropy.threshold` gates opt-in heuristic findings while structural detectors remain eligible; [ADR-0010](decisions/ADR-0010-entropy-gating-policy.md) owns the policy | +| Keyword pre-filters for high-noise detectors | Planned | High | Add Aho-Corasick pre-filter keywords / context anchors to detectors that still scan every chunk; validate them against the accuracy corpus | +| Broaden OpenAI key coverage | Delivered in `v1.7.0` | High | Legacy and service-account variants ship alongside project keys, anchored on provider-specific structure | +| GitHub fine-grained PAT support | Delivered in `v1.7.0` | High | Fine-grained personal access tokens (`github_pat_`) are detected | +| Trusted Shopify issuer configuration | Delivered after `v1.7.0` | High | The CLI accepts only an explicit, validated operator-controlled store origin; finding-controlled domains never select the target | +| Tighten Anthropic key regex | Planned | Medium | Anchor exact length and trailing marker; distinguish admin from standard keys | +| Mailgun & JWT format breadth | Planned | Medium | Add the alternate Mailgun key format; broaden JWT matching to pretty-printed / base64-variant headers and optional padding | +| Supabase service-role detection | Planned | Medium | Detect service-role JWTs (`role: service_role`) in addition to management PATs; the existing `sbp_` detector is explicitly documented as a Management PAT | +| Bounded result buffering | Delivered in `v1.7.0` | Medium | Scan result memory is explicitly bounded before verification | +| Accuracy benchmark suite | Planned | High | Curated true/false corpus measuring precision and recall per detector, run in CI to guard against regressions | ### Acceptance Criteria - [ ] Verified-mode false-positive rate is measured on the benchmark corpus and meets the `<5%` target -- [ ] The configured entropy threshold gates findings engine-wide, not only for custom rules +- [x] The configured entropy threshold gates findings engine-wide, not only for custom rules (`v1.7.0`) - [ ] Telegram/Discord (and other previously keyword-less detectors) no longer fire on unrelated numeric/base64 strings in the corpus -- [ ] okta, shopify, and bitbucket findings produce verified/inactive results end-to-end -- [ ] OpenAI legacy/service-account keys and GitHub `github_pat_` tokens are detected +- [x] Shopify findings reach verification only through a validated operator-trusted store origin; Okta and Bitbucket retain their detector-to-verifier context wiring (post-`v1.7.0`, awaiting the next release) +- [x] OpenAI legacy/service-account keys and GitHub `github_pat_` tokens are detected (`v1.7.0`) - [ ] Detector test coverage stays ≥95% for every touched detector ### Exit Criteria -GitHub Release published with `v1.7.0` tag. +GitHub Release published with `v1.8.0` tag. --- @@ -459,7 +494,7 @@ GitHub Release published with `v1.7.0` tag. **Goal:** Grow coverage of frequently-leaked, high-blast-radius credential types, prioritizing secrets whose exposure causes the most damage. Every new detector with a public verification endpoint ships with its verifier. -**Duration:** 5-6 weeks | **Version:** `v1.8.0` | **Status:** Planned +**Duration:** 5-6 weeks | **Version:** `v1.9.0` | **Status:** Planned ### Deliverables @@ -482,7 +517,7 @@ GitHub Release published with `v1.7.0` tag. ### Exit Criteria -GitHub Release published with `v1.8.0` tag. +GitHub Release published with `v1.9.0` tag. --- @@ -490,13 +525,13 @@ GitHub Release published with `v1.8.0` tag. **Goal:** Deepen the verification differentiator. Harden the verification engine, verify more credential classes, and — for live secrets — tell users what the credential can actually reach so they can triage blast radius. -**Duration:** 5-6 weeks | **Version:** `v1.9.0` | **Status:** Planned +**Duration:** 5-6 weeks | **Version:** `v1.10.0` | **Status:** Planned ### Deliverables | Task | Priority | Description | |------|----------|-------------| -| Per-provider rate limiting, caching & backoff | Critical | Replace the single global token-bucket limiter with per-provider limits, response caching to avoid re-verifying identical secrets, and exponential backoff/retry on transient failures | +| Provider policy, caching & transient backoff | Critical | Build on the current global plus per-detector token-bucket limiters and one bounded, safe `Retry-After` replay: add provider-specific policies, response caching to avoid re-verifying identical secrets, and broader safe transient backoff | | Canary-safe verification | High | Recognize well-known decoy/canary credential formats and skip live verification for them, so a scan never triggers an alert on someone else's planted token | | Active private-key verification | High | Where a safe check exists, derive the public key from a detected private key and confirm liveness/association; introduce a distinct "verified key material" status that does not overstate access | | Credential impact analysis | High | Opt-in: for a verified secret, enumerate its effective permissions and reachable resources, starting with the highest-value providers, so users understand blast radius — not just that a secret is live | @@ -504,7 +539,7 @@ GitHub Release published with `v1.8.0` tag. ### Acceptance Criteria -- [ ] Per-provider limits and response caching are verified under load; transient failures retry with backoff +- [ ] Provider-specific policies and response caching are verified under load; eligible transient failures retry with bounded backoff - [ ] Known canary credential formats are never sent to a live endpoint - [ ] Private-key findings can reach a "verified key material" status without implying broader access - [ ] Impact analysis produces a permission/resource summary for at least the top-priority providers @@ -512,7 +547,7 @@ GitHub Release published with `v1.8.0` tag. ### Exit Criteria -GitHub Release published with `v1.9.0` tag. +GitHub Release published with `v1.10.0` tag. --- @@ -520,7 +555,7 @@ GitHub Release published with `v1.9.0` tag. **Goal:** Reach secrets wherever they live — collaboration platforms and org-scale code hosting — now that the detection/verification core is strong. -**Duration:** 5-6 weeks | **Version:** `v1.10.0` | **Status:** Planned +**Duration:** 5-6 weeks | **Version:** `v1.11.0` | **Status:** Planned ### Deliverables @@ -532,7 +567,7 @@ GitHub Release published with `v1.9.0` tag. | `scan confluence` command | Critical | Space filtering, attachment scanning | | `scan jira` command | Critical | Project filtering, JQL support | | Org-scale repository enumeration | High | Scan every repository (and its history) under an organization/group via the hosting API, instead of a single local/remote repo at a time | -| Slack file content scanning | Medium | Fetch and scan file attachments via the Files API, completing the `--include-files` flag that is currently accepted but a no-op | +| Slack file content scanning | Delivered after `v1.7.0` | Fetch and scan bounded text-like attachments via the Files API when `--include-files` is set | | SourceMetadata fields | High | Space, page, issue key, org/repo context in findings | | Additional platform sources | Low | API-collection platforms, CI systems, and search clusters as demand warrants | | Tests | High | `httptest.NewServer` mocks | @@ -545,11 +580,11 @@ GitHub Release published with `v1.9.0` tag. - [ ] Both Cloud and Server editions supported - [ ] HTML content properly extracted from Confluence storage format - [ ] An entire organization's repositories can be enumerated and scanned from a single command -- [ ] Slack file attachments are fetched and scanned when `--include-files` is set +- [x] Slack file attachments are fetched and scanned when `--include-files` is set (post-`v1.7.0` hardening wave) ### Exit Criteria -GitHub Release published with `v1.10.0` tag. +GitHub Release published with `v1.11.0` tag. --- @@ -557,7 +592,7 @@ GitHub Release published with `v1.10.0` tag. **Goal:** Persistent SQLite-based inventory tracking secrets across scans. -**Duration:** 4-5 weeks | **Version:** `v1.11.0` | **Status:** Planned +**Duration:** 4-5 weeks | **Version:** `v1.12.0` | **Status:** Planned ### Deliverables @@ -584,7 +619,7 @@ GitHub Release published with `v1.10.0` tag. ### Exit Criteria -GitHub Release published with `v1.11.0` tag. +GitHub Release published with `v1.12.0` tag. --- @@ -592,7 +627,7 @@ GitHub Release published with `v1.11.0` tag. **Goal:** Generate and deploy decoy credentials that alert on unauthorized use. -**Duration:** 3-4 weeks | **Version:** `v1.12.0` | **Status:** Planned +**Duration:** 3-4 weeks | **Version:** `v1.13.0` | **Status:** Planned ### Deliverables @@ -618,7 +653,7 @@ GitHub Release published with `v1.11.0` tag. ### Exit Criteria -GitHub Release published with `v1.12.0` tag. +GitHub Release published with `v1.13.0` tag. --- @@ -633,8 +668,8 @@ Earlier reviews recorded behaviors that the documentation or public interface pr | Supabase service-role JWT not detected (only management PAT) | Phase 9 | | Unbounded in-memory result buffering | Phase 9 | | `--remediation-format brief\|full` flag not implemented | Phase 9 (minor) | -| Per-provider rate limiting, verification caching, exponential backoff/retry | Phase 11 | -| Slack file scanning (`--include-files` is a no-op) | Phase 12 | +| Provider-specific verification policy, caching, and broader bounded transient backoff | Phase 11 | +| Slack file scanning | Delivered after `v1.7.0`; `--include-files` is explicit opt-in | > **Minor item — `--remediation-format`:** today only a boolean `--remediation` flag exists; the `brief|full` variant referenced in the Phase 6 deliverables and the verification guide is unimplemented. Small UX task, folded into Phase 9. @@ -765,12 +800,13 @@ Source packages (no formal standard, but visible gaps): | `v1.4.0` | Phase 8.3 | Scan summary, `init` command, colored table, security patches | 2026-04-08 | | `v1.5.0` | Phase 8.4 | False positive reduction, ADO.NET support | 2026-04-09 | | `v1.6.0` | Phase 8.5 | GitHub Marketplace Action, `github` output format, config wiring | 2026-05-25 | -| `v1.7.0` | Phase 9 | Detection accuracy & false-positive reduction | — | -| `v1.8.0` | Phase 10 | Detector library expansion | — | -| `v1.9.0` | Phase 11 | Verification depth & credential impact | — | -| `v1.10.0` | Phase 12 | Source expansion (Confluence/Jira, org-scale) | — | -| `v1.11.0` | Phase 13 | Secrets inventory (SQLite) | — | -| `v1.12.0` | Phase 14 | Honeytokens | — | +| `v1.7.0` | Review Remediation | Full-project review remediation, correctness, security, and CI hardening | 2026-07-20 | +| `v1.8.0` | Phase 9 | Detection accuracy & false-positive reduction | 2026-08-11 | +| `v1.9.0` | Phase 10 | Detector library expansion | — | +| `v1.10.0` | Phase 11 | Verification depth & credential impact | — | +| `v1.11.0` | Phase 12 | Source expansion (Confluence/Jira, org-scale) | — | +| `v1.12.0` | Phase 13 | Secrets inventory (SQLite) | — | +| `v1.13.0` | Phase 14 | Honeytokens | — | | `v2.x.x` | Future | ML detection, SaaS platform, Vault | Ongoing | > **Note on v1.1.0 / v1.2.0:** Phase 6 (Remediation Guidance) and Phase 7 (Slack Scanning) were completed and merged into `main`, but no `v1.1.0` or `v1.2.0` git tags were ever created. The features shipped as part of the `v1.3.0` release. The version slots are preserved here to keep the phase-to-version mapping consistent. @@ -796,7 +832,7 @@ Source packages (no formal standard, but visible gaps): |--------|----------------|-----------------| | GitHub Stars | 500+ | 2,000+ | | Contributors | 5+ | 15+ | -| Detector count | 64 (achieved) | 120+ | +| Detector count | 65 (achieved) | 120+ | | Verifier count | 54 (achieved) | 80+ | | Source count | 6 (fs, git, container, S3, GCS, Slack) | 9+ | @@ -816,14 +852,18 @@ Source packages (no formal standard, but visible gaps): ## Master Review — Documented-but-Unimplemented Gaps -> This section is the **detailed reference** for the gaps found in the 2026-05-22 full-project review. Each item is now scheduled into a planned phase (see the concise [Documented Gap Traceability](#documented-gap-traceability) index); the "Owning phase" column below shows where each is delivered. Each remains **not yet implemented** — the documentation or public interface promises the feature but the code does not deliver it. +> **Historical snapshot — superseded 2026-08-11:** this inventory preserves the findings as they were recorded during the 2026-05-22 review; it is not a current implementation-status dashboard. The `v1.7.0` release and subsequent review-remediation wave commits have closed or reclassified multiple rows. Current behavior is defined by the executable registry/capability contracts, their CI tests, the user manuals, and the changelog. Future audits must create a new dated snapshot rather than silently rewriting this one. +> +> The working `review/FIX-PLAN.md` remains an untracked audit artifact and is intentionally not treated as release metadata. Git commits are the authoritative closure record for the remediation waves. + +> At capture time, this section was the **detailed reference** for gaps found in the 2026-05-22 full-project review. The "Owning phase" column records the scheduling decision made in that snapshot; "not implemented" statements below describe the code at that date and must not be read as current status. | # | Gap | One-line description | Area affected | Owning phase | |---|-----|----------------------|---------------|--------------| -| 1 | **Slack file scanning** | `--include-files` flag is accepted and documented but is a no-op; the `SlackSource` never fetches file content from the Slack Files API. | `internal/source/slack/slack.go`, `docs/guides/slack-scanning.md`, CHANGELOG v1.2.0 | Phase 12 | -| 2 | **Per-provider rate limiting, verification caching, exponential backoff/retry** | The verifier engine has a single global token-bucket rate limiter; there is no per-provider limit, no response caching, and no retry with backoff. Phase 8 deliverables and the ROADMAP claim per-provider rate limiting is implemented. | `internal/verifier/engine.go`, Phase 8 deliverables table, v1.3.0 highlights | Phase 11 | +| 1 | **Slack file scanning** | Historical 2026-05-22 finding: `--include-files` was a no-op. Closed after `v1.7.0` with explicit opt-in, bounded text downloads, and `files:read`. | `internal/source/slack/slack.go`, `docs/guides/slack-scanning.md`, CHANGELOG Unreleased | Delivered | +| 2 | **Provider-specific verification policy, caching, and broader bounded transient backoff** | Historical snapshot: the engine formerly had only one global limiter and no retry. It now enforces global plus per-detector admission and one bounded safe `Retry-After` replay; response caching, provider-tuned policies, and wider eligible transient backoff remain planned. | `internal/verifier/engine.go`, Phase 8 deliverables table, v1.3.0 highlights | Phase 11 | | 3 | **`--remediation-format brief\|full` flag** | Only a boolean `--remediation` flag exists; the two-value `brief\|full` variant mentioned in Phase 6 deliverables and `docs/guides/secret-verification.md` is not implemented. | `cmd/scan_common.go`, Phase 6 deliverables table | Phase 9 (minor) | | 4 | **Engine-level entropy-threshold gating** | The `detection.entropy.threshold` config value is read and displayed in scan summaries, but the detection engine does not gate findings on it; only custom YAML rules apply their own per-rule entropy threshold. | `internal/engine/`, `internal/config/`, `docs/guides/configuration.md` | Phase 9 | -| 5 | **okta / shopify / bitbucket live verification** | The verifiers for these three providers exist and compile, but their `Verify()` implementations read `ExtraData` keys (`domain`, `store_domain`, `username` respectively) that no detector currently emits. The findings will always produce a `StatusUnverified` result until the corresponding detectors are updated to populate those `ExtraData` fields. | `internal/verifier/okta/`, `internal/verifier/shopify/`, `internal/verifier/bitbucket/`, and their matching detectors | Phase 9 | +| 5 | **Shopify trusted-store configuration** | Shopify access-token formats do not identify their issuing store. The verifier deliberately ignores finding-controlled domains and remains `StatusUnverified` until a validated, operator-controlled store origin is available. The read-only 2026-07 GraphQL verifier contract is prepared; Okta and Bitbucket companion context is already wired. | `internal/verifier/shopify/`, `internal/detector/shopify/`, and scan configuration | Phase 9 | | 6 | **Supabase real service-role JWT detection** | The `supabase` detector matches the `sbp_` prefix which identifies Supabase Management PATs, not service-role JWTs. Service-role JWTs (`eyJ...` with `role: service_role`) are not detected; the name `supabase-service-key` implies broader coverage than is implemented. | `internal/detector/supabase/supabase_key.go`, README detector table | Phase 9 | | 7 | **Unbounded in-memory result buffering** | The scan engine collects all raw findings into a single in-memory slice before passing them to the verification phase — there is no streaming path and no cap on the buffer size. This is acceptable for typical inputs but is a known limitation for very large or adversarial inputs (e.g., a repository engineered to maximise regex matches) where memory consumption could become excessive. Streaming verification or an explicit buffer cap is planned. | `internal/engine/engine.go` | Phase 9 | diff --git a/docs/architecture/01-COMPETITIVE-ANALYSIS.md b/docs/architecture/01-COMPETITIVE-ANALYSIS.md index 870054b..9d87385 100644 --- a/docs/architecture/01-COMPETITIVE-ANALYSIS.md +++ b/docs/architecture/01-COMPETITIVE-ANALYSIS.md @@ -187,7 +187,7 @@ While existing open-source tools (TruffleHog, Gitleaks) are strong in certain ar | **Filesystem** | Yes | Yes | Yes | No | Yes | **Yes** | | **Container Images** | Yes | No | No | No | Yes | **Yes** | | **Cloud Sources** | Yes (S3, GCS) | No | No | No | No | **Yes (Phase 5)** | -| **SaaS Scanning** | Yes (Slack, Jira) | No | No | No | Public monitoring | **Yes (Slack, shipped `v1.2.0`; message text only — file-attachment scanning still planned)** | +| **SaaS Scanning** | Yes (Slack, Jira) | No | No | No | Public monitoring | **Yes (Slack messages shipped `v1.2.0`; bounded text attachments delivered post-`v1.7.0`)** | | **Aho-Corasick** | Yes | Yes | No | Unknown | Unknown | **Yes** | | **Entropy Analysis** | Yes | As filter | Yes | No | With ML | **Yes (hybrid)** | | **SARIF Output** | No | Yes | No | Native | Yes | **Yes** | diff --git a/docs/architecture/03-ARCHITECTURE.md b/docs/architecture/03-ARCHITECTURE.md index b07a7db..550fc19 100644 --- a/docs/architecture/03-ARCHITECTURE.md +++ b/docs/architecture/03-ARCHITECTURE.md @@ -28,11 +28,11 @@ flowchart LR E4[Custom Rules] end - subgraph Verifiers["Verifiers (54 verifiers, 51 packages, 84.4% coverage)"] + subgraph Verifiers["Verification (39 direct-live + 9 context-required + 6 format-only)"] V1[AWS STS] V2[GitHub API] V3[Slack API] - V4[Stripe, JWT, ...] + V4[Stripe, context & format checks] end subgraph Output["Output"] @@ -61,7 +61,7 @@ classDiagram <> +Type() string +Chunks(ctx) chan Chunk - +Validate() error + +Validate(ctx context.Context) error } class Detector { @@ -145,7 +145,7 @@ type Source interface { Chunks(ctx context.Context) <-chan Chunk // Validate checks that the source is accessible and valid. - Validate() error + Validate(ctx context.Context) error } // Chunk represents the smallest unit of data to be scanned. @@ -271,8 +271,8 @@ type Finding struct { Severity Severity `json:"severity"` // Secret data - Raw string `json:"raw,omitempty"` // Only with --show-raw - Redacted string `json:"redacted"` // Masked + Raw string `json:"-"` // Dedicated opt-in wire type only + Redacted string `json:"redacted"` // Masked // Location SourceMetadata SourceMetadata `json:"source"` @@ -281,15 +281,19 @@ type Finding struct { Verification VerificationResult `json:"verification"` // Timestamp - DetectedAt time.Time `json:"detected_at"` + DetectedAt time.Time `json:"detected_at,omitempty"` // Custom marshal omits zero // Entropy - Entropy float64 `json:"entropy,omitempty"` + Entropy float64 `json:"entropy,omitempty"` + EntropyCalculated bool `json:"-"` // Preserves a calculated 0.0 on the wire // Additional context - ExtraData map[string]string `json:"extra_data,omitempty"` + ExtraData map[string]string `json:"-"` // Dedicated opt-in wire type only } +// SetEntropy marks a value as calculated, including a legitimate 0.0. +func (f *Finding) SetEntropy(value float64) + // Severity represents the finding severity level. type Severity int @@ -301,6 +305,11 @@ const ( ) ``` +The custom JSON contract omits absent entropy and zero `DetectedAt`, but emits a +legitimately calculated entropy of `0.0`. `Raw` and `ExtraData` remain +type-level excluded; the JSON formatter's explicit `--show-raw` wire type is the +only path that can add them. + --- ### 2.6 Component Interaction Sequence @@ -327,7 +336,7 @@ sequenceDiagram Worker->>Detector: Scan(ctx, data) Detector-->>Worker: []RawFinding Worker-->>Engine: Write to results channel - Note over Worker: Entropy is calculated and attached
to the finding (display-only at engine level) + Note over Worker: Entropy is calculated and attached;
opt-in heuristic findings are threshold-gated end end opt Verification enabled @@ -347,9 +356,9 @@ sequenceDiagram ```mermaid flowchart TD Source["SOURCE\n(chunks channel)"] --> Jobs["JOBS CHANNEL\n(buffered chan)"] - Jobs --> W1["Worker 1\nAC Match → Regex → Entropy score (display)"] - Jobs --> W2["Worker 2..N-1\nAC Match → Regex → Entropy score (display)"] - Jobs --> WN["Worker N\nAC Match → Regex → Entropy score (display)"] + Jobs --> W1["Worker 1\nAC Match → Regex → Entropy policy"] + Jobs --> W2["Worker 2..N-1\nAC Match → Regex → Entropy policy"] + Jobs --> WN["Worker N\nAC Match → Regex → Entropy policy"] W1 --> Results["RESULTS CHANNEL\n(buffered chan)"] W2 --> Results WN --> Results @@ -369,8 +378,8 @@ type Config struct { // Detection Detectors []detector.Detector EnableEntropy bool - // EntropyThreshold is validated and stored, but at the engine level - // entropy is display-only. Threshold gating applies only to custom rules. + // EntropyThreshold gates only heuristic detectors that explicitly opt in; + // structural provider detectors remain independent of entropy. EntropyThreshold float64 // Default: 4.0 ShowRaw bool // Expose raw secret content on Finding.Raw @@ -409,7 +418,7 @@ stateDiagram-v2 state ParallelScan { [*] --> ACPreFilter: Aho-Corasick pre-filtering ACPreFilter --> DetectorScan: Call matching detectors' Scan() - DetectorScan --> EntropyCalc: Entropy score (display-only) + DetectorScan --> EntropyCalc: Score and opt-in heuristic gate EntropyCalc --> WriteResult: Write to results channel } } @@ -628,11 +637,13 @@ flowchart TD F --> G{"Finding\nexists?"} G -->|"No"| H[Skip] G -->|"Yes"| I["Calculate Shannon entropy\n(if EnableEntropy = true)"] - I --> J["Attach entropy score to Finding\n(display-only — no gating)"] - J --> K["Finding → Results channel"] + I --> J{"Detector opts into\nentropy gating?"} + J -->|"No"| K["Finding → Results channel"] + J -->|"Yes, score meets threshold"| K + J -->|"Yes, score below threshold"| H ``` -> **Note on entropy gating:** At the engine level, entropy is **display-only**. The computed value is attached to the `Finding.Entropy` field for human review; the engine never suppresses or demotes a finding based on entropy score. The exception is **custom rules**: a YAML rule with an `entropy:` field does gate matches — only candidates whose entropy meets or exceeds the rule's threshold are emitted. A global entropy gate for built-in detectors is planned but not yet implemented (see [ROADMAP — Known Gaps](../../docs/05-ROADMAP.md)). +> **Entropy policy:** The engine attaches the computed value to every finding for review. The configured threshold suppresses only heuristic detectors that explicitly opt into entropy gating — currently `generic-api-key`. Structural provider detectors such as AWS and GitHub never depend on this threshold. Custom rules apply their own independent per-rule `entropy` threshold. ### 6.2 Shannon Entropy Calculation diff --git a/docs/architecture/04-MIDTERM-FEATURES-ANALYSIS.md b/docs/architecture/04-MIDTERM-FEATURES-ANALYSIS.md index 45ec1f8..879d5f3 100644 --- a/docs/architecture/04-MIDTERM-FEATURES-ANALYSIS.md +++ b/docs/architecture/04-MIDTERM-FEATURES-ANALYSIS.md @@ -158,8 +158,8 @@ leakwatch scan slack --token xoxb-... [flags] --exclude-channels Channels to exclude --since string Oldest message date (RFC3339) --include-dms Include direct messages (default: false) - --include-files Scan uploaded files (default: true) - --rate-limit float Max API req/sec (default: 20) + --include-files Scan uploaded text files (default: false) + --rate-limit float Per-operation request cap (default: 0; safe operation-specific limits) ``` ### Implementation Tasks diff --git a/docs/architecture/05-VERIFIER-ANALYSIS.md b/docs/architecture/05-VERIFIER-ANALYSIS.md index 4f5ef08..1fab8a2 100644 --- a/docs/architecture/05-VERIFIER-ANALYSIS.md +++ b/docs/architecture/05-VERIFIER-ANALYSIS.md @@ -1,23 +1,24 @@ # Leakwatch - Secret Verifier Analysis -> **Document Version:** 2.0 -> **Date:** 2026-03-25 +> **Document Version:** 2.1 +> **Date:** 2026-08-11 > **Status:** Completed ## 1. Current State -Leakwatch has **64 built-in detectors (60 packages)** and **54 verifiers (51 packages)** covering 84.4% of all detectors. +Leakwatch has **65 built-in detectors (61 packages)** and **54 registered verifier implementations (51 packages)**. Registry coverage is 54/65 (83.1%), but this is not live capability: 39 detectors are direct-live, 9 require trusted issuer, region, or companion context, 6 are format-only, and 11 have no verifier. All counts are verified by inspecting `detector.Register(` and `verifier.Register(` call sites. | Metric | Value | Source | |--------|-------|--------| -| Total Detectors | 64 (60 packages) | `grep -r "detector.Register" internal/detector/` | +| Total Detectors | 65 (61 packages) | runtime registry guard (`cmd/stats_test.go`) | | Verifiers Implemented | 54 (51 packages) | `grep -r "verifier.Register" internal/verifier/` | -| Verification Coverage | 54/64 (84.4%) | | -| Live API Verifiers | 48 | Make real network/SDK calls | +| Registered-verifier coverage | 54/65 (83.1%) | Not a live-capability metric | +| Direct-live capabilities | 39 | Make real network/SDK calls in the normal production path | +| Context-required capabilities | 9 | Auth0, GitLab, Grafana, Twilio, Shopify, GitHub PAT/OAuth, Datadog, Snyk | | Format-Only Verifiers | 6 | Structural/format checks only | -| Detectors Without Verifiers | 10 | Listed in section 2 | +| Detectors Without Verifiers | 11 | Listed in section 2 | | Verifier Architecture | `init()` + compile-time registration via `verifier.Register()` | | | Verification Interface | `Verifier.Verify(ctx context.Context, raw detector.RawFinding) finding.VerificationResult` | `internal/verifier/verifier.go` | @@ -26,12 +27,12 @@ All counts are verified by inspecting `detector.Register(` and `verifier.Registe All current verifiers follow a consistent pattern: - **AWS** (`aws-access-key-id`): Uses STS `GetCallerIdentity` with the key pair (requires both Access Key ID in `Raw` and Secret Access Key in `RawV2`). Returns account/ARN metadata on success. -- **GitHub** (`github-token`): HTTP GET to `https://api.github.com/user` with `Bearer` token. Returns login username on success. +- **GitHub** (`github-token`, `github-oauth-token`): GitHub.com and GHES share token formats. Production has no operator-controlled trusted origin yet, so both return `unverified` without a request. With a trusted origin, PAT/`gho_`/`ghu_` use `/user` and validate optional scope/expiry headers, `ghs_` uses `/installation/repositories`, and `ghr_` remains unverified because exchange would rotate it. - **Slack** (`slack-token`): HTTP POST to `https://slack.com/api/auth.test` with `Bearer` token. Returns team/user metadata on success. ## 2. Verifier Classification -All 64 detectors are classified into tiers based on verification feasibility. **All 54 verifiers listed in Tiers 1–3 are implemented and registered.** +All 65 detectors are classified by verification feasibility. **All 54 implementations listed in Tiers 1–3 are registered, but nine implementations require trusted issuer, region, or companion context rather than providing a direct-live capability.** ### Tier 1 --- Easy (Simple API call, single credential, no extra context) @@ -39,15 +40,15 @@ These detectors can be verified with a single HTTP request using only the detect | # | Detector ID | API Endpoint | Method | Auth Header | Complexity | Priority | Notes | |---|-------------|-------------|--------|-------------|------------|----------|-------| -| 1 | `github-token` | `https://api.github.com/user` | GET | `Bearer {token}` | Easy | P0 | Returns login username on success | +| 1 | `github-token` | Trusted GitHub.com/GHES `/user` | GET | `Bearer {token}` | Context required | P0 | GitHub.com and GHES share token formats; the operator selects the issuer with the command-line-only `--verifier-origin` flag. Valid optional scope/expiry headers enrich the active result | | 2 | `slack-token` | `https://slack.com/api/auth.test` | POST | `Bearer {token}` | Easy | P0 | Returns team/user metadata on success | | 3 | `openai-api-key` | `https://api.openai.com/v1/models` | GET | `Bearer {token}` | Easy | P0 | Returns model list; 401 if invalid | | 4 | `anthropic-api-key` | `https://api.anthropic.com/v1/models` | GET | `x-api-key: {token}` | Easy | P0 | Requires `anthropic-version` header | -| 5 | `gitlab-pat` | `https://{host}/api/v4/user` | GET | `PRIVATE-TOKEN: {token}` | Easy | P0 | Returns user info; 401 if invalid. Defaults to `gitlab.com`, honoring a co-located self-hosted instance host (`ExtraData["host"]`) when present | +| 5 | `gitlab-pat` | Operator-trusted GitLab.com/self-managed `/api/v4/user`, then optional `/personal_access_tokens/self` | GET | `PRIVATE-TOKEN: {token}` | Context required | P0 | Repository content never chooses the origin; the command-line-only `--verifier-origin` flag supplies explicit trust. Only the `glpat-` subtype supports the identity probe; self metadata safely enriches scopes/expiry after active proof. A `401` is inactive only with the standard invalid-token JSON body, not a DPoP challenge | | 6 | `sendgrid-api-key` | `https://api.sendgrid.com/v3/scopes` | GET | `Bearer {token}` | Easy | P0 | Needs no specific scope, so a restricted-permission key is never misread as revoked; 401 = invalid, other unexpected statuses fall through to a verify error rather than a false negative | | 7 | `digitalocean-token` | `https://api.digitalocean.com/v2/account` | GET | `Bearer {token}` | Easy | P0 | Returns account info | | 8 | `cloudflare-api-token` | `https://api.cloudflare.com/client/v4/user/tokens/verify` | GET | `Bearer {token}` | Easy | P0 | Dedicated verify endpoint | -| 9 | `newrelic-api-key` | `https://api.newrelic.com/v2/users.json` | GET | `Api-Key: {token}` | Easy | P0 | Returns user list; 401 if invalid | +| 9 | `newrelic-api-key` | US/EU NerdGraph `/graphql` | POST (read-only query) | `Api-Key: {token}` | Regional fallback | P0 | `requestContext { userId }`; inactive only when every documented region returns 401; 403 is inconclusive | | 10 | `heroku-api-key` | `https://api.heroku.com/account` | GET | `Bearer {token}` | Easy | P0 | Requires `Accept: application/vnd.heroku+json; version=3` | | 11 | `notion-token` | `https://api.notion.com/v1/users/me` | GET | `Bearer {token}` | Easy | P0 | Requires `Notion-Version` header | | 12 | `telegram-bot-token` | `https://api.telegram.org/bot{token}/getMe` | GET | Token in URL path | Easy | P0 | Token is part of URL, not header | @@ -60,17 +61,17 @@ These detectors can be verified with a single HTTP request using only the detect | 19 | `npm-token` | `https://registry.npmjs.org/-/npm/v1/user` | GET | `Bearer {token}` | Easy | P1 | Returns user profile | | 20 | `huggingface-token` | `https://huggingface.co/api/whoami-v2` | GET | `Bearer {token}` | Easy | P1 | Returns user/org info | | 21 | `airtable-pat` | `https://api.airtable.com/v0/meta/whoami` | GET | `Bearer {token}` | Easy | P1 | Dedicated whoami endpoint | -| 22 | `snyk-api-key` | `https://api.snyk.io/rest/self?version=2024-04-22` | GET | `Authorization: token {key}` | Easy | P1 | Returns user info | +| 22 | `snyk-api-key` | Trusted regional/private `/rest/self` | GET | `Authorization: token {key}` | Context required | P1 | Snyk API origins are region/deployment-specific and are selected with the command-line-only `--verifier-origin` flag | | 23 | `figma-pat` | `https://api.figma.com/v1/me` | GET | `X-FIGMA-TOKEN: {token}` | Easy | P1 | Returns user info | | 24 | `postmark-server-token` | `https://api.postmarkapp.com/server` | GET | `X-Postmark-Server-Token: {token}` | Easy | P1 | Returns server info | -| 25 | `grafana-api-key` | `https://grafana.com/api/user` | GET | `Bearer {token}` | Easy | P1 | Note: instance URL needed for self-hosted | +| 25 | `grafana-api-key` | Issuing instance `/api/access-control/user/permissions` | GET | `Bearer {token}` | Context required | P1 | Explicit `--verifier-origin` or backward-compatible `--grafana-instance-url`; no trusted URL means no request and `unverified`; repository-provided URLs are never trusted | | 26 | `doppler-token` | `https://api.doppler.com/v3/me` | GET | `Bearer {token}` | Easy | P1 | Returns user/workplace info | | 27 | `sonarcloud-token` | `https://sonarcloud.io/api/authentication/validate` | GET | Basic `{token}:` | Easy | P2 | Dedicated validate endpoint; Basic auth with token as username | | 28 | `pypi-api-token` | `https://upload.pypi.org/legacy/` | POST | Basic `__token__:{token}` | Easy | P2 | Check via upload endpoint returns 400 (active) vs 403 (invalid) | | 29 | `deepseek-api-key` | `https://api.deepseek.com/models` | GET | `Bearer {token}` | Easy | P2 | Returns model list; 401 if invalid | | 30 | `launchdarkly-sdk-key` | `https://app.launchdarkly.com/api/v2/caller-identity` | GET | `Authorization: {key}` | Easy | P2 | Returns caller identity | -**Total Tier 1: 30 detectors (all implemented)** +**Total Tier 1: 30 implementations — 26 direct-live and 4 context-required (`github-token`, `gitlab-pat`, `grafana-api-key`, `snyk-api-key`)** ### Tier 2 --- Medium (Needs extra context, specific auth flow, or domain extraction) @@ -81,23 +82,23 @@ These require either extracting additional information from the finding context | 1 | `aws-access-key-id` | AWS STS `GetCallerIdentity` | SDK | HMAC-signed | Medium | P0 | Needs both Access Key ID (`Raw`) + Secret Access Key (`RawV2`) | | 2 | `stripe-api-key-live` | `https://api.stripe.com/v1/charges?limit=1` | GET | Basic `{key}:` | Medium | P0 | Live key -- verification must be read-only; use minimal-scope endpoint | | 3 | `stripe-api-key-test` | `https://api.stripe.com/v1/charges?limit=1` | GET | Basic `{key}:` | Medium | P1 | Test key -- lower risk but same flow as live | -| 4 | `twilio-api-key` | `https://api.twilio.com/2010-04-01/Accounts.json` | GET | Basic `{apiKeySID}:{apiKeySecret}` | Medium | P1 | Paired as API Key SID (`Raw`) + API Key Secret (`ExtraData["api_key_secret"]`) — NOT Account SID + Auth Token, since the detector captures an API Key SID (`SK...`), which only authenticates with its own paired secret. Scoped to `ExtraData["account_sid"]` when available. Missing secret → `unverified`, never a guessed inactive | +| 4 | `twilio-api-key` | Trusted regional Twilio origin + Accounts probe | GET | Basic `{apiKeySID}:{apiKeySecret}` | Context required | P1 | `Raw` is the opaque value from an explicit API Key Secret assignment, paired one-to-one with a nearby explicitly assigned non-secret `SK...` Key SID. `ExtraData` contains only SIDs; bare SIDs are not findings. The operator selects the regional origin with `--verifier-origin`. Accounts/Keys permissions differ for Main, Standard and Restricted keys, so `403` is inconclusive; only `401` is inactive | | 5 | `mailgun-api-key` | `https://api.mailgun.net/v3/domains`, retrying `https://api.eu.mailgun.net/v3/domains` | GET | Basic `api:{key}` | Medium | P1 | Mailgun keys carry no region marker; the US host is probed first and the EU host is retried only if US reports the key inactive, so a live EU-only key is never misreported as dead | -| 6 | `shopify-access-token` | `https://{shop}.myshopify.com/admin/api/2024-01/shop.json` | GET | `X-Shopify-Access-Token: {token}` | Medium | P1 | Requires `ExtraData["store_domain"]`, which the detector does not currently populate — findings resolve to `unverified` until this wiring lands (tracked in the ROADMAP) | +| 6 | `shopify-access-token` | Trusted store origin + `/admin/api/2026-07/graphql.json` | POST | `X-Shopify-Access-Token: {token}` | Context required | P1 | Uses a read-only `shop { name }` identity query with strict JSON/GraphQL response validation. Finding-controlled domains are ignored; the operator selects an exact `*.myshopify.com` origin with `--verifier-origin`. Only `401` on that store is inactive | | 7 | `okta-api-token` | `https://{domain}/api/v1/users/me` | GET | `SSWS {token}` | Medium | P1 | Domain is captured by the detector from a co-located org domain in `ExtraData["domain"]`; without it, the result is `unverified` (never a false invalid) | | 8 | `databricks-token` | `https://{workspace-host}/api/2.0/preview/scim/v2/Me` | GET | `Bearer {token}` | Medium | P1 | Workspace host captured by the detector in `ExtraData["host"]` (a Databricks PAT only authenticates against its own workspace host); without it, the result is `unverified` | -| 9 | `github-oauth-token` | `https://api.github.com/user` | GET | `Bearer {token}` | Medium | P1 | Same endpoint as github-token but OAuth tokens may have limited scopes | -| 10 | `auth0-management-token` | `https://{tenant}/api/v2/` | GET | `Bearer {token}` | Medium | P2 | JWT-format; the tenant host is decoded from the token's own `iss` claim (unverified signature — used only for routing, not trust), falling back to `unverified` when the claim is absent | -| 11 | `datadog-api-key` | `https://api.datadoghq.com/api/v1/validate` | GET | `DD-API-KEY: {key}` | Medium | P2 | Dedicated validate endpoint; may also need Application Key for full validation | +| 9 | `github-oauth-token` | Trusted GitHub.com/GHES subtype-specific identity endpoint | GET | `Bearer {token}` | Context required | P1 | `gho_`/`ghu_` → `/user`; `ghs_` → `/installation/repositories`; `ghr_` is never exchanged because verification would rotate it; `--verifier-origin` selects the issuer | +| 10 | `auth0-management-token` | Operator-trusted Auth0 tenant/custom `/api/v2/clients` | GET | `Bearer {token}` | Context required | P2 | JWT claims are untrusted scanner input and never choose the request origin; `--verifier-origin` provides explicit operator trust | +| 11 | `datadog-api-key` | Trusted Datadog site `/api/v1/validate` | GET | `DD-API-KEY: {key}` | Context required | P2 | Keys are site-bound; `--verifier-origin` accepts only exact official Datadog API hosts | | 12 | `terraform-cloud-token` | `https://app.terraform.io/api/v2/account/details` | GET | `Bearer {token}` | Medium | P2 | Standard Bearer auth; some tokens may be for Terraform Enterprise (custom URL) | -| 13 | `supabase-service-key` | `https://api.supabase.com/v1/projects` | GET | `Bearer {token}` | Medium | P2 | Fixed Supabase Management API host — no project ref, no `apikey` header, no per-project context needed | +| 13 | `supabase-service-key` | `https://api.supabase.com/v1/projects` | GET | `Bearer {token}` | Medium | P2 | Detects a Management API personal access token (`sbp_`), despite the stable legacy ID; fixed provider host, `401` inactive, `403` inconclusive | | 14 | `rubygems-api-key` | `https://rubygems.org/api/v1/api_key.json` | GET | `Authorization: {key}` | Medium | P2 | Returns key metadata | | 15 | `bitbucket-app-password` | `https://api.bitbucket.org/2.0/user` | GET | Basic `{username}:{app-password}` | Medium | P2 | Username captured by the detector in `ExtraData["username"]` | | 16 | `dockerhub-pat` | `https://hub.docker.com/v2/user/` | GET | `Bearer {token}` | Medium | P2 | Docker Hub PATs authenticate the v2 API directly; no login/JWT-exchange step and no username needed | | 17 | `teams-webhook` | `POST {webhookURL}` | POST | None (URL is the credential) | Medium | P1 | Sends minimal payload; 400 treated as active (valid URL, empty payload rejected). Live probe — no real message content posted. | | 18 | `infura-api-key` | `https://mainnet.infura.io/v3/{token}` | POST | Token in URL path | Medium | P1 | JSON-RPC `web3_clientVersion` call. Consumes a small amount of API quota. | -**Total Tier 2: 18 detectors (all implemented)** +**Total Tier 2: 18 implementations — 13 direct-live and 5 context-required (`twilio-api-key`, `shopify-access-token`, `github-oauth-token`, `auth0-management-token`, `datadog-api-key`)** > **Note:** `hashicorp-vault-token` was previously listed in this tier (Tier 2 P2) but has no verifier implementation. It has been moved to Tier 4 (no verifier). See section 2 Tier 4 for rationale. `coinbase-api-key` was previously listed here but is a format-only (Tier 3) check — see below. @@ -118,7 +119,7 @@ These detectors have verifier implementations that perform structural/format val ### Tier 4 --- No Verifier Implemented -These 10 detectors currently have no verifier. The reasons range from "no public API" to "side effects on verification" to "planned but not yet built." +These 11 detectors currently have no verifier. The reasons range from "no public API" to "side effects on verification" to "provider/issuer cannot be determined safely." | # | Detector ID | Reason / Status | |---|-------------|-----------------| @@ -132,22 +133,22 @@ These 10 detectors currently have no verifier. The reasons range from "no public | 8 | `discord-webhook-url` | Discord incoming-webhook URLs. Any call would POST a message to a real channel (side effect). Read-only verification is not possible. | | 9 | `jwt` | JSON Web Tokens. Cannot verify the signature without the signing key. Can only check expiry and structural validity — no live state can be confirmed. Planned. | | 10 | `hashicorp-vault-token` | Vault tokens. Live check requires the Vault server address extracted from context, which is typically not available in a static finding. Planned. | +| 11 | `structured-config-secret` | Strong field context identifies a secret role, but the provider and issuer cannot be determined safely. | -**Total Tier 4: 10 detectors (no verifier)** +**Total Tier 4: 11 detectors (no verifier)** ### Verified Tier Summary | Tier | Count | Description | |------|-------|-------------| -| Tier 1 — Live (Easy) | 30 | Simple Bearer/API-key, single HTTP request | -| Tier 2 — Live (Medium) | 16 | Context extraction, multi-step auth, or SDK | -| Tier 2b — Live (Side-effect probe) | 2 | `teams-webhook`, `infura-api-key` — live but consume quota or trigger side effects | +| Direct live | 39 | Safe live request is available in the normal production path | +| Requires trusted issuer/region/companion context | 9 | Registered, but a bare finding cannot safely make a live request | | Tier 3 — Format-Only | 6 | Structural validation; no network call | -| Tier 4 — No Verifier | 10 | Not implemented | -| **Total Detectors** | **64** | | -| **Total Verifiers** | **54** | Live: 48 · Format-only: 6 | +| Tier 4 — No Verifier | 11 | Not implemented | +| **Total Detectors** | **65** | | +| **Registered verifier implementations** | **54** | Direct-live: 39 · context-required: 9 · format-only: 6 | -> **Coverage:** 54/64 = **84.4%** +> **Registry coverage:** 54/65 = **83.1%**. **Direct-live capability:** 39/65 = **60.0%**. **Notes:** - `teams-webhook` (`internal/verifier/teams`): Live HTTP POST probe. A 400 response (valid URL but empty payload rejected) is treated as active. This is a deliberate non-destructive probe — no readable message is posted. @@ -173,7 +174,7 @@ All 5 sprints have been completed. Two additional verifiers (`teams-webhook`, `i | Post-roadmap additions | 2 | 54/63 (85.7%) | teams-webhook, infura-api-key (live probes added in fix/wire-custom-rules-and-inline-ignore) | | **Total** | **51** | **54/63 (85.7%)** | | -> **Historical snapshot:** the sprint table above reflects the state as of 2026-05-22, before the `discord-webhook-url` detector was added and the `coinbase-api-key` verifier was reclassified from a Tier 2 live check to a Tier 3 format-only check (it never made a real live call — see Tier 3, section 2). **Current totals are 64 detectors / 54 verifiers = 84.4% coverage** (section 1); the sprint-by-sprint history is preserved here unchanged for traceability. +> **Historical snapshot:** the sprint table above reflects the state as of 2026-05-22, before the `discord-webhook-url` and `structured-config-secret` detectors were added and before capability was separated from registry presence. **Current totals are 65 detectors / 54 registered implementations = 83.1% registry coverage, with 39 direct-live capabilities** (section 1); the sprint-by-sprint history is preserved here unchanged for traceability. ## 4. Security Considerations @@ -202,7 +203,7 @@ Implementation: Use `golang.org/x/time/rate` (already a dependency) with a per-v ### Timeout Handling - All verifier HTTP requests MUST respect the `context.Context` deadline. -- Default per-request timeout: **10 seconds**. +- Default per-finding verification-operation timeout: **10 seconds**, including bounded provider-region fallback. - If the verification API is unreachable or times out, return `StatusVerifyError` (not `StatusVerifiedInactive`). A network failure does not mean the secret is invalid. - Implement exponential backoff for transient failures (429, 503) with a maximum of 2 retries. diff --git a/docs/assets/banner.html b/docs/assets/banner.html index 48806e4..91e6acb 100644 --- a/docs/assets/banner.html +++ b/docs/assets/banner.html @@ -77,8 +77,8 @@
- 64 detectors· - 54 live verifiers· + 65 detectors· + 39 direct-live checks· 6 sources· 5 output formats
diff --git a/docs/assets/banner.png b/docs/assets/banner.png index 60742ef..f8ba556 100644 Binary files a/docs/assets/banner.png and b/docs/assets/banner.png differ diff --git a/docs/assets/banner.svg b/docs/assets/banner.svg new file mode 100644 index 0000000..5cb531d --- /dev/null +++ b/docs/assets/banner.svg @@ -0,0 +1,45 @@ + + Leakwatch + Find leaked secrets before attackers do. Detect, verify, and report across code, Git history, containers, and cloud. + + + + + + + + + + + + + + + + + + + + + + + Leakwatch + + + Find leaked secrets + before attackers do. + Detect · verify · report — across code, git history, containers & cloud. + + + + 65 detectors + · + 39 direct-live checks + · + 6 sources + · + 5 output formats + + + github.com/HodeTech/Leakwatch · MIT + diff --git a/docs/decisions/ADR-0005-pattern-matching.md b/docs/decisions/ADR-0005-pattern-matching.md index 2cdcb77..9dcec32 100644 --- a/docs/decisions/ADR-0005-pattern-matching.md +++ b/docs/decisions/ADR-0005-pattern-matching.md @@ -1,6 +1,6 @@ # ADR-0005: Pattern Matching Strategy — Aho-Corasick Hybrid -- **Status:** Accepted +- **Status:** Amended by [ADR-0010](ADR-0010-entropy-gating-policy.md) - **Date:** 2026-03-24 - **Decision Makers:** Project team @@ -65,3 +65,4 @@ Secret scanning requires matching thousands of different patterns (regex) agains ## Related Decisions - [ADR-0001: Programming Language](ADR-0001-programming-language.md) — Go's regex weakness triggering this decision +- [ADR-0010: Entropy Gating Policy](ADR-0010-entropy-gating-policy.md) — amends the tertiary entropy policy; the Aho-Corasick and regex decisions remain active diff --git a/docs/decisions/ADR-0010-entropy-gating-policy.md b/docs/decisions/ADR-0010-entropy-gating-policy.md new file mode 100644 index 0000000..90a5e01 --- /dev/null +++ b/docs/decisions/ADR-0010-entropy-gating-policy.md @@ -0,0 +1,60 @@ +# ADR-0010: Entropy Gating Policy + +- **Status:** Accepted +- **Date:** 2026-08-11 +- **Decision Makers:** Project team +- **Amends:** The tertiary entropy policy in [ADR-0005](ADR-0005-pattern-matching.md) + +## Context + +ADR-0005 originally made engine-level entropy display-only and reserved threshold filtering for custom rules. Leakwatch now has a bounded engine policy for heuristic built-in findings as well. Leaving the accepted decision unchanged would make the ADR chain contradict the executable contract and encourage future regressions. + +Entropy is useful for arbitrary credential-like assignments, but it is not reliable evidence for structurally identified provider credentials. Applying one global threshold to every detector would suppress legitimate low-entropy secrets. Conversely, calculating a score without allowing explicitly heuristic detectors to use it leaves a documented false-positive control ineffective. + +## Decision + +When `detection.entropy.enabled` is true, the engine calculates and attaches Shannon entropy to every non-empty finding. The configured `detection.entropy.threshold` is a suppression gate only when all of these conditions hold: + +1. The detector explicitly implements the engine's `EntropyBased` contract and returns `true`. +2. If the detector also implements `EntropyGated`, that per-finding policy returns `true`. +3. The finding's entropy is below the configured threshold. + +Built-in structural provider findings never become ineligible merely because their entropy is low. The current built-in opt-in detector is `generic-api-key`; its explicit provider-context findings may bypass the threshold through `EntropyGated`, while ordinary heuristic assignments remain gated. + +Custom YAML rules keep their independent per-rule `entropy` threshold. Their filtering contract is not replaced by the built-in detector gate. + +The implementation in `internal/engine/engine.go` and its executable tests are authoritative for the interface and ordering details. Current user-facing behavior is documented in the English user manual; translations and supplemental guides must preserve that contract. + +## Alternatives Considered + +### Keep entropy display-only for all built-in detectors + +Rejected. This would preserve known high-noise heuristic matches and make the configured global threshold misleading. + +### Gate every detector with the global threshold + +Rejected. Entropy is not a validity signal for many provider-issued formats and would introduce false negatives. + +### Maintain an engine-owned detector ID allowlist + +Rejected. Optional detector interfaces keep the policy local, testable, and extensible without coupling the engine to registry names. + +## Consequences + +### Positive + +- The global setting has a precise, executable effect on heuristic built-in findings. +- Structural credentials remain recall-safe regardless of entropy. +- Mixed detectors can refine the decision per finding. +- Documentation and tests can assert one stable opt-in contract. + +### Negative + +- New heuristic detectors must deliberately choose and test their entropy policy. +- Entropy calculations remain visible on findings even when they do not control eligibility. +- Custom rules and built-in detectors retain separate threshold mechanisms that documentation must distinguish. + +## Related Decisions + +- [ADR-0005: Pattern Matching Strategy](ADR-0005-pattern-matching.md) — Aho-Corasick and regex pipeline remain active; entropy clause amended here +- [ADR-0008: Concurrency Model](ADR-0008-concurrency-model.md) — worker execution model in which the gate runs diff --git a/docs/decisions/ADR-0011-product-trust-boundaries.md b/docs/decisions/ADR-0011-product-trust-boundaries.md new file mode 100644 index 0000000..e452dc3 --- /dev/null +++ b/docs/decisions/ADR-0011-product-trust-boundaries.md @@ -0,0 +1,66 @@ +# ADR-0011: Product Enhancement Trust Boundaries + +- **Status:** Accepted +- **Date:** 2026-08-11 +- **Decision Makers:** Project team + +## Context + +The post-review product backlog mixes low-risk improvements with features that can suppress findings, transmit credentials, or publish artifacts externally. Treating every unchecked roadmap item as equivalent would encourage unsafe implementation merely to mark it complete. The product needs explicit shipped/experimental/planned states and acceptance boundaries. + +## Decision + +The current status is: + +| Capability | Status | Contract | +|---|---|---| +| Slack text-file attachments | **Shipped** | Explicit `--include-files`; `files:read`; Slack-owned HTTPS only; operation-scoped limiters; byte-bounded backpressure; bounded size/retry; UTF-8 text classification; file-ID deduplication; token-safe terminal errors/logs. | +| GitHub/GitLab scope and expiry metadata | **Shipped** | Best-effort, schema-validated metadata only after an authoritative active proof; malformed or unavailable enrichment never changes active/inactive evidence. | +| VSIX build and icon | **Shipped** | Lockfile-pinned local package command and CI artifact; the VSIX contains only the bundled runtime, license/readme/changelog, manifest, and 256×256 icon. | +| Visual Studio Marketplace publication | **Experimental** | Manual `main`-only workflow, explicit boolean input, one reviewed VSIX artifact, environment-scoped `VSCE_PAT`, and a least-privilege metadata audit proving both the secret's environment provenance and externally configured reviewers/self-review/branch policy. No automatic tag publication. | +| Provider contract freshness audit | **Shipped** | Allowlisted primary references and review dates in the capability manifest; weekly 180-day plus live link/fragment gate; synthetic adversarial tests remain mandatory. | +| Baseline/snapshot suppression | **Planned** | Not exposed until the threat model and versioned format below are implemented and tested. | +| Coinbase legacy live HMAC verification | **Planned** | Remains format-only until key/secret correlation is assignment-aware, unambiguous, secret-safe, and backed by a safe read-only provider contract. | +| Live provider canaries | **Planned** | No credentials in default CI; provider-specific, protected, explicit opt-in only after the controls in the provider-audit standard are satisfied. | + +### Baseline threat model and required design + +A baseline can hide a real credential, so a plain detector/path allowlist or an unkeyed hash is not acceptable. The future implementation must: + +1. use a versioned, strict schema and reject unknown versions/fields, duplicate identities, malformed paths, and oversized inputs; +2. store no raw or redacted credential material; +3. identify entries with `HMAC-SHA-256(key, "leakwatch-baseline/v1" || length-prefixed detector ID || length-prefixed normalized source identity || length-prefixed raw credential)` using an external organization-controlled, high-entropy key that is never written to the baseline; the canonical length prefixes prevent concatenation ambiguity; +4. fail closed when the key is absent, malformed, or unavailable and never silently treat an unreadable baseline as empty; +5. make creation/update an explicit user command, write atomically with restrictive permissions, and never auto-approve changes during a scan; +6. apply suppression before live verification so an accepted legacy secret is not transmitted merely because it is baselined, while keeping suppressed counts visible in the summary; and +7. ship adversarial tests for tampering, replay to another path/detector, low-entropy offline guessing, symlink/output attacks, merge conflicts, and secret leakage in every formatter/log/error. + +Repository write access alone must not let an attacker create a valid suppression entry; the external HMAC key is the second trust boundary. Teams that cannot protect such a key must use existing explicit ignore mechanisms with normal code review rather than baseline mode. + +## Alternatives Considered + +### Ship a plaintext or SHA-256 baseline immediately + +Rejected. It either stores sensitive material or permits offline guessing of low-entropy credentials, and repository writers can suppress new findings without a second trust boundary. + +### Automatically publish every extension tag + +Rejected. Marketplace publication is an external, difficult-to-reverse action. A reviewable artifact and protected manual promotion keep build and publication separate. + +### Add shared live credentials to scheduled CI + +Rejected. It expands secret distribution and may trigger billing, alerts, mutations, or provider abuse controls. Contract tests plus dated official-document audits are the safe default. + +### Guess Coinbase key/secret pairs by proximity + +Rejected. Ambiguous pairing can send the wrong secret and turn an authentication rejection into a false inactive result. Format-only is the truthful current capability. + +## Consequences + +Low-risk enhancements ship with executable acceptance tests, while suppression, live credentials, and external publication retain explicit gates. Some roadmap items remain planned; this is deliberate product truthfulness, not an implementation claim. Future work must amend this ADR when it changes any trust boundary or status. + +## Related Documents + +- [Provider Verification Contract Audits](../standards/05-PROVIDER-CONTRACT-AUDITS.md) +- [ADR-0009: GitHub Marketplace Action](ADR-0009-github-marketplace-action.md) +- [Verification Coverage](../user-manuals/en/verification/verification-coverage.md) diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 068972d..87baa6a 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -11,7 +11,7 @@ An ADR is a short document that records the context, rationale, and consequences Each ADR follows the structure below: - **Title:** `ADR-NNNN: ` -- **Status:** Proposed | **Accepted** | Amended | Rejected | Deprecated +- **Status:** Proposed | **Accepted** | Superseded | Amended | Rejected | Deprecated - **Context:** The situation and problem that led to the decision - **Decision:** The decision made and its rationale - **Alternatives Considered:** Options evaluated and reasons for rejection @@ -25,8 +25,10 @@ Each ADR follows the structure below: | [ADR-0002](ADR-0002-cli-frame.md) | CLI Framework: Cobra + Viper | Accepted | 2026-03-24 | | [ADR-0003](ADR-0003-git-library.md) | Git Library: go-git | Accepted | 2026-03-24 | | [ADR-0004](ADR-0004-plugin-architecture.md) | Plugin Architecture: Compile-Time | Accepted | 2026-03-24 | -| [ADR-0005](ADR-0005-pattern-matching.md) | Pattern Matching: Aho-Corasick Hybrid | Accepted | 2026-03-24 | +| [ADR-0005](ADR-0005-pattern-matching.md) | Pattern Matching: Aho-Corasick Hybrid | Amended by ADR-0010 | 2026-03-24 | | [ADR-0006](ADR-0006-container-library.md) | Container Library: go-containerregistry | Accepted | 2026-03-24 | | [ADR-0007](ADR-0007-license.md) | License: MIT | Accepted | 2026-03-24 | | [ADR-0008](ADR-0008-concurrency-model.md) | Concurrency: Worker Pool | Accepted | 2026-03-24 | | [ADR-0009](ADR-0009-github-marketplace-action.md) | GitHub Marketplace Action: Location & Runtime | Accepted | 2026-05-24 | +| [ADR-0010](ADR-0010-entropy-gating-policy.md) | Entropy Gating Policy | Accepted | 2026-08-11 | +| [ADR-0011](ADR-0011-product-trust-boundaries.md) | Product Enhancement Trust Boundaries | Accepted | 2026-08-11 | diff --git a/docs/guides/README.md b/docs/guides/README.md new file mode 100644 index 0000000..2b6ef8f --- /dev/null +++ b/docs/guides/README.md @@ -0,0 +1,27 @@ +# Leakwatch Supplemental Guides + +> **Document Version:** 1.0 +> **Date:** 2026-08-11 +> **Status:** Approved + +--- + +The English pages under [`docs/user-manuals/en`](../user-manuals/en/) are the +authoritative source for current Leakwatch product behavior. Their Turkish +counterparts under [`docs/user-manuals/tr`](../user-manuals/tr/) are reviewed +translations, and `tools/site-build` compiles both trees into the generated +`site/js/manuals/*.js` bundles. + +Files in this directory are supplemental deep dives. They may provide longer +operational examples, implementation rationale, or contributor-oriented detail, +but they do not own CLI flags, defaults, exit codes, detector/verifier capability +claims, or other product contracts. Each guide links to the relevant canonical +manual page; when behavior changes, update EN, TR, the supplemental guide where +useful, and the generated site output in the same commit. + +Do not edit `site/js/manuals/*.js` by hand. Regenerate and validate them with: + +```bash +cd tools/site-build +go run . -strict +``` diff --git a/docs/guides/ci-cd-integration.md b/docs/guides/ci-cd-integration.md index 67f81b6..e3d5af5 100644 --- a/docs/guides/ci-cd-integration.md +++ b/docs/guides/ci-cd-integration.md @@ -1,11 +1,13 @@ # Leakwatch - CI/CD Integration Guide -> **Document Version:** 1.0 -> **Date:** 2026-03-24 +> **Document Version:** 1.1 +> **Date:** 2026-08-11 > **Status:** Approved --- +> **Documentation role:** Supplemental CI/CD deep dive. The [CI/CD user manual](../user-manuals/en/ci-cd/github-action.md) is authoritative for current inputs, defaults, and exit behavior. + ## Table of Contents 1. [Why Secret Scanning in CI/CD?](#1-why-secret-scanning-in-cicd) @@ -75,7 +77,7 @@ Leakwatch provides a ready-to-use GitHub Action (root `action.yml`, published on | `working-directory` | `.` | Directory to run the scan from | | `sarif-upload` | `false` | Upload SARIF results to GitHub Code Scanning. Requires `format: sarif` and `permissions: security-events: write` | | `fail-on-findings` | `true` | Fail the workflow step when Leakwatch reports findings (exit code 1). When `false`, a `::warning::` annotation is emitted instead so the scan does not block the pipeline. Hard errors (exit code >= 2) always fail the step regardless of this setting | -| `version` | `latest` | Leakwatch version to install: `latest` or a release tag such as `v1.6.0` | +| `version` | `latest` | Leakwatch version to install: `latest` or the current release tag, `v1.8.0` | | `release-repo` | `HodeTech/Leakwatch` | GitHub repository (`owner/name`) to download the release binary from. Override only for forks or self-hosted mirrors | **Outputs:** @@ -285,7 +287,7 @@ permissions: pull-requests: read env: - LEAKWATCH_VERSION: 'v1.6.0' + LEAKWATCH_VERSION: 'v1.8.0' jobs: # On PRs, scan only the changed files @@ -586,7 +588,7 @@ Create a `.pre-commit-config.yaml` file in your project root: # .pre-commit-config.yaml repos: - repo: https://github.com/HodeTech/Leakwatch - rev: v1.6.0 + rev: v1.8.0 hooks: - id: leakwatch ``` @@ -735,7 +737,7 @@ leakwatch scan fs . --min-severity critical ### 7.2 Reducing False Positives with `--only-verified` -Leakwatch ships with 54 verifiers (51 packages) covering 84.4% of all detector types, confirming whether discovered secrets are still active via API calls. With the `--only-verified` parameter, you can report only verified (active) secrets: +Leakwatch has 39 direct-live capabilities, 9 context-required capabilities, 6 format-only validators, and 11 detector types without a verifier. With `--only-verified`, you report only findings actually confirmed `verified_active`: ```bash # Report only verified secrets @@ -745,7 +747,7 @@ leakwatch scan git . --only-verified leakwatch scan git . --since-commit HEAD~1 --only-verified --min-severity medium ``` -**Note:** With 54 verifiers (51 packages) and 84.4% coverage, `--only-verified` is effective for most secret types. However, the remaining ~16% of detectors (e.g., generic private keys) do not have verifiers, so those findings will not be reported. For full coverage, periodically run a full scan without `--only-verified`. +**Note:** `--only-verified` intentionally excludes context-required findings when their context is absent, every format-only finding, all 11 no-verifier types, and inconclusive `verify_error` results. It is a prioritization view, not a complete security scan. Run without it for full detection coverage. > **Important:** `--only-verified` has **no effect** when `--no-verify` is also set (or when `verification.enabled: false` in config), because verification is disabled and all findings remain in the `unverified` state. To use `--only-verified` meaningfully, ensure verification is enabled by omitting `--no-verify` and setting `verification.enabled: true` in your config. diff --git a/docs/guides/cloud-scanning.md b/docs/guides/cloud-scanning.md index 3688bea..b96c4e0 100644 --- a/docs/guides/cloud-scanning.md +++ b/docs/guides/cloud-scanning.md @@ -1,11 +1,13 @@ # Leakwatch - Cloud Storage Scanning Guide -> **Document Version:** 1.0 -> **Date:** 2026-03-24 +> **Document Version:** 1.1 +> **Date:** 2026-08-11 > **Status:** Approved --- +> **Documentation role:** Supplemental operations deep dive. The [cloud-storage user manual](../user-manuals/en/scanning/cloud-storage.md) is authoritative for current source behavior and flags. + ## Table of Contents 1. [Why Scan Cloud Storage?](#1-why-scan-cloud-storage) diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md index 5dbebf2..e755e95 100644 --- a/docs/guides/configuration.md +++ b/docs/guides/configuration.md @@ -1,11 +1,13 @@ # Leakwatch - Configuration Guide -> **Document Version:** 1.0 -> **Date:** 2026-03-24 +> **Document Version:** 1.1 +> **Date:** 2026-08-11 > **Status:** Approved --- +> **Documentation role:** Supplemental configuration deep dive. The [configuration user manual](../user-manuals/en/configuration/config-file.md) is authoritative for current keys, defaults, and precedence. + ## 1. Overview Leakwatch offers a flexible configuration system. Configuration values can be read from multiple sources and are merged by overriding each other. @@ -80,13 +82,10 @@ detection: # Shannon entropy threshold (float64, 0.0–8.0). Default: 4.0 # - # NOTE — this value is currently display-only for built-in detectors. - # The engine computes and attaches entropy scores to findings for context, - # but does NOT gate or filter built-in detector findings on this threshold. - # Engine-level gating is planned — see the ROADMAP - # "Documented-but-Unimplemented Gaps" §4 (Engine-level entropy-threshold gating). - # - # Per-rule entropy thresholds in `custom-rules:` DO filter custom-rule matches. + # The engine computes and attaches entropy scores to every finding. The + # threshold gates only built-in heuristic detectors that explicitly opt in + # (currently `generic-api-key`); structural provider detectors are never + # suppressed by this setting. Custom rules apply their own per-rule entropy. threshold: 4.0 # ── Verification Settings ───────────────────────────────────────── @@ -97,7 +96,8 @@ verification: # Default: true enabled: true - # Maximum timeout for a single verification request. + # Maximum timeout for one finding's verification operation, including any + # bounded provider-region fallback requests. # Go duration format: "10s", "30s", "1m" # Default: 10s timeout: 10s @@ -132,7 +132,7 @@ filter: # ── Output Settings ───────────────────────────────────────────── output: # Output format. - # Valid values: json, sarif, csv, table + # Valid values: json, sarif, csv, table, github # Default: json format: json @@ -153,7 +153,7 @@ output: # ── Custom Rules ────────────────────────────────────────────── custom-rules: - # Leakwatch ships with 64 detectors (60 packages). Use YAML custom rules + # Leakwatch ships with 65 detectors (61 packages). Use YAML custom rules # to detect secrets not covered by built-in detectors. # Each rule can contain the following fields: - id: "internal-api-key" @@ -186,7 +186,7 @@ custom-rules: | Field | Type | Default | Description | |-------|------|---------|-------------| | `enabled` | bool | `true` | Enable/disable entropy analysis | -| `threshold` | float64 | `4.0` | Entropy threshold (0.0–8.0). Display-only for built-in detectors; filters custom-rule matches only | +| `threshold` | float64 | `4.0` | Entropy threshold (0.0–8.0). Gates opt-in heuristic detectors (currently `generic-api-key`); structural detectors are unaffected | **What is entropy?** Shannon entropy measures the degree of randomness in a text. Secrets typically have high entropy (4.0+) because they consist of random characters. Normal code has lower entropy. @@ -210,7 +210,7 @@ flowchart LR | Field | Type | Default | Description | |-------|------|---------|-------------| | `enabled` | bool | `true` | Enable/disable verification | -| `timeout` | duration | `10s` | Timeout per request | +| `timeout` | duration | `10s` | Timeout for one finding's complete verification operation, including bounded provider-region fallback | | `concurrency` | int | `4` | Number of parallel verifications | | `rate-limit` | float64 | `10.0` | Max verification API requests per second (must be positive when enabled) | @@ -225,7 +225,7 @@ flowchart LR | Field | Type | Default | Description | |-------|------|---------|-------------| -| `format` | string | `json` | `json`, `sarif`, `csv`, `table` | +| `format` | string | `json` | `json`, `sarif`, `csv`, `table`, `github` | | `file` | string | `""` | Output file (empty = stdout) | | `show-raw` | bool | `false` | Show secret content without masking | | `severity-threshold` | string | `low` | Minimum reporting level | @@ -245,6 +245,8 @@ flowchart LR Slack scanning is configured entirely via CLI flags and environment variables. There is no `slack:` key in `.leakwatch.yaml`; YAML support for Slack options is planned for a future release. +The bot needs `channels:read`, `channels:history`, `groups:read`, and `groups:history`. Add `im:read`, `im:history`, `mpim:read`, and `mpim:history` for `--include-dms`, and `files:read` for `--include-files`. + Use CLI flags directly when running `scan slack`: ```bash @@ -255,7 +257,7 @@ leakwatch scan slack \ --channels "engineering,devops" \ --exclude-channels "random" \ --since "2026-01-01" \ - --rate-limit 20 \ + --rate-limit 0.8 \ --min-severity medium ``` @@ -265,9 +267,9 @@ leakwatch scan slack \ | `--channels` | all | Comma-separated channel names to scan | | `--exclude-channels` | none | Comma-separated channel names to skip | | `--since` | none | Scan messages posted after this date (`YYYY-MM-DD`) | -| `--include-dms` | `false` | Scan direct messages (requires `im:history` / `mpim:history` scopes) | -| `--include-files` | `false` | **Not yet implemented** (planned). Deprecated and hidden; has no effect — only message text is scanned | -| `--rate-limit` | `20` | Maximum Slack API requests per second | +| `--include-dms` | `false` | Scan direct messages (requires `im:read`, `im:history`, `mpim:read`, and `mpim:history`) | +| `--include-files` | `false` | Download and scan bounded text-like attachments; requires Slack `files:read` | +| `--rate-limit` | `0` | Optional per-operation Slack request cap; zero retains safe operation-specific defaults (history `1/60`, list/file/download higher) | > **Security note:** Always provide the Slack token via the `LEAKWATCH_SLACK_TOKEN` environment variable rather than the `--token` flag in automated environments, to avoid token exposure in process listings or CI logs. @@ -697,3 +699,4 @@ leakwatch scan fs . --log-level debug 2>&1 | head -5 | Architecture design | [Architecture Document](../architecture/03-ARCHITECTURE.md) | | ADR: CLI framework (Cobra + Viper) | [ADR-0002](../decisions/ADR-0002-cli-frame.md) | | ADR: Pattern matching | [ADR-0005](../decisions/ADR-0005-pattern-matching.md) | +| ADR: Entropy gating | [ADR-0010](../decisions/ADR-0010-entropy-gating-policy.md) | diff --git a/docs/guides/container-scanning.md b/docs/guides/container-scanning.md index 195887b..b05e7d5 100644 --- a/docs/guides/container-scanning.md +++ b/docs/guides/container-scanning.md @@ -1,11 +1,13 @@ # Leakwatch - Container Image Scanning Guide -> **Document Version:** 1.0 -> **Date:** 2026-03-24 +> **Document Version:** 1.1 +> **Date:** 2026-08-11 > **Status:** Approved --- +> **Documentation role:** Supplemental container-scanning deep dive. The [container-images user manual](../user-manuals/en/scanning/container-images.md) is authoritative for current commands and source behavior. + ## Table of Contents 1. [Why Scan Container Images?](#1-why-scan-container-images) diff --git a/docs/guides/custom-rules.md b/docs/guides/custom-rules.md index b986294..38f6035 100644 --- a/docs/guides/custom-rules.md +++ b/docs/guides/custom-rules.md @@ -1,11 +1,13 @@ # Leakwatch - Custom Rules Guide -> **Document Version:** 1.0 -> **Date:** 2026-03-24 +> **Document Version:** 1.1 +> **Date:** 2026-08-11 > **Status:** Approved --- +> **Documentation role:** Supplemental rule-authoring deep dive. The [custom-rules user manual](../user-manuals/en/detectors/custom-rules.md) is authoritative for the supported schema and runtime behavior. + ## Table of Contents 1. [Why Custom Rules?](#1-why-custom-rules) @@ -21,12 +23,12 @@ ## 1. Why Custom Rules? -Leakwatch ships with **64 detectors (60 packages)** covering major cloud providers (AWS, GCP, Azure, DigitalOcean, Heroku, Vercel), AI platforms (OpenAI, Anthropic, Hugging Face, DeepSeek), CI/CD tools (GitHub, GitLab, CircleCI, Bitbucket), databases (PostgreSQL, MongoDB, Redis, Snowflake, RabbitMQ), and dozens of SaaS services. For secrets not covered by built-in detectors, you can define custom rules in YAML. +Leakwatch ships with **65 detectors (61 packages)** covering major cloud providers (AWS, GCP, Azure, DigitalOcean, Heroku, Vercel), AI platforms (OpenAI, Anthropic, Hugging Face, DeepSeek), CI/CD tools (GitHub, GitLab, CircleCI, Bitbucket), databases (PostgreSQL, MongoDB, Redis, Snowflake, RabbitMQ), structured configuration, and dozens of SaaS services. For secrets not covered by built-in detectors, you can define custom rules in YAML. Custom rules are especially useful in the following cases: - **Internal company token formats:** API keys or session tokens generated by internal systems usually do not conform to a standard pattern -- **Long-tail SaaS services:** Niche or regional SaaS products not yet covered by the 64 detectors (60 packages) +- **Long-tail SaaS services:** Niche or regional SaaS products not yet covered by the 65 detectors (61 packages) - **Database credential patterns:** Internal databases that use connection strings in different formats - **Compliance requirements:** Additional pattern scans mandated by corporate security policies - **Temporary rules:** Quickly scanning for a known secret format after a security incident diff --git a/docs/guides/docker-usage.md b/docs/guides/docker-usage.md index 8cd2637..daf6b7e 100644 --- a/docs/guides/docker-usage.md +++ b/docs/guides/docker-usage.md @@ -1,11 +1,13 @@ # Leakwatch - Docker Usage Guide -> **Document Version:** 1.0 -> **Date:** 2026-03-24 +> **Document Version:** 1.1 +> **Date:** 2026-08-11 > **Status:** Approved --- +> **Documentation role:** Supplemental Docker operations deep dive. The [Docker user manual](../user-manuals/en/ci-cd/docker-usage.md) is authoritative for current image usage and examples. + ## 1. Quick Start Leakwatch is available as a container image from GitHub Container Registry. No installation required -- just `docker run`. @@ -504,7 +506,7 @@ flowchart TD subgraph LW["Leakwatch Process (non-root)"] CMD["CLI Command\n(scan fs/git/image/s3/gcs)"] ENG["Detection Engine\n(Aho-Corasick + Regex + Entropy)"] - VER["Verification Engine\n(54 verifiers (51 packages), 84.4% coverage)"] + VER["Verification Engine\n(39 direct-live + 9 context-required + 6 format-only)"] FMT["Output Formatter"] end end diff --git a/docs/guides/getting-started.md b/docs/guides/getting-started.md index 1dd67d2..08b1fc2 100644 --- a/docs/guides/getting-started.md +++ b/docs/guides/getting-started.md @@ -1,20 +1,22 @@ # Leakwatch - Getting Started Guide -> **Document Version:** 1.0 -> **Date:** 2026-03-24 +> **Document Version:** 1.1 +> **Date:** 2026-08-11 > **Status:** Approved --- +> **Documentation role:** Supplemental product tour. The [getting-started user manual](../user-manuals/en/getting-started/introduction.md) is authoritative for current installation and CLI behavior. + ## 1. What is Leakwatch? Leakwatch is a high-performance, open source (MIT) security tool that detects, verifies, and reports leaked secrets (API keys, passwords, certificates) in codebases, Git histories, and container images. **Key features:** -- **64 detectors (60 packages) + unlimited YAML custom rules** -- Covers major cloud providers, AI platforms, CI/CD tools, databases, and SaaS services out of the box, with YAML custom rules for anything else +- **65 detectors (61 packages) + unlimited YAML custom rules** -- Covers major cloud providers, AI platforms, CI/CD tools, databases, structured configuration, and SaaS services out of the box, with YAML custom rules for anything else - **Hybrid detection engine** -- Aho-Corasick pre-filter, regex validation, and Shannon entropy analysis for a low false positive rate -- **Secret verification** -- 54 verifiers (51 packages) with 84.4% verification coverage confirm whether discovered secrets are still active via API calls (AWS, GitHub, Slack, Stripe, and more) +- **Truthful verification capability** -- 39 direct-live checks, 9 context-required checks, and 6 offline format validators; the 54 registered implementations are never overstated as live coverage - **Multi-source support** -- Filesystem, Git repository, container images, S3, GCS, parallel multi-repo, and Slack - **Flexible output** -- JSON, SARIF, CSV, and table formats - **Single binary, zero dependencies** -- Runs on every platform, no Docker daemon required @@ -36,11 +38,11 @@ flowchart LR E3["Entropy\nAnalysis"] end - subgraph Verification["Secret Verification\n(54 verifiers (51 packages), 84.4% coverage)"] + subgraph Verification["Secret Verification\n(39 direct-live + 9 context-required + 6 format-only)"] V1["AWS STS"] V2["GitHub API"] V3["Slack API"] - V4["Stripe, JWT, ..."] + V4["Stripe, context & format checks"] end Sources -->|Chunks| E1 @@ -277,7 +279,7 @@ leakwatch scan repos https://github.com/org/repo1 https://github.com/org/repo2 - ### 4.7 Slack Workspace Scan (`scan slack`) -Scans message text across channels in a Slack workspace for leaked secrets. (Scanning of uploaded files is planned but not yet implemented.) +Scans message text across channels in a Slack workspace for leaked secrets. Add `--include-files` (and Slack's `files:read` scope) to scan bounded text-like attachments. ```bash # Scan a Slack workspace diff --git a/docs/guides/git-scanning.md b/docs/guides/git-scanning.md index 5575507..0279370 100644 --- a/docs/guides/git-scanning.md +++ b/docs/guides/git-scanning.md @@ -1,11 +1,13 @@ # Leakwatch - Git Repository Scanning Guide -> **Document Version:** 1.0 -> **Date:** 2026-03-24 +> **Document Version:** 1.1 +> **Date:** 2026-08-11 > **Status:** Approved --- +> **Documentation role:** Supplemental Git-scanning deep dive. The [Git-history user manual](../user-manuals/en/scanning/git-history.md) is authoritative for current target and history semantics. + ## 1. Overview The `scan git` command scans the entire commit history of a Git repository to find leaked secrets. Unlike a filesystem scan, which only checks current file contents, a Git scan examines every blob across all commits -- uncovering secrets that were added and later deleted. diff --git a/docs/guides/secret-verification.md b/docs/guides/secret-verification.md index b0adb49..c9688fc 100644 --- a/docs/guides/secret-verification.md +++ b/docs/guides/secret-verification.md @@ -1,17 +1,20 @@ # Leakwatch - Secret Verification Guide -> **Document Version:** 1.1 -> **Date:** 2026-03-25 +> **Document Version:** 1.3 +> **Date:** 2026-08-11 > **Status:** Approved --- +> **Documentation role:** Supplemental verifier-contract deep dive. The [verification user manual](../user-manuals/en/verification/how-verification-works.md) and its generated capability table are authoritative for current user-facing behavior. + ## 1. What is Secret Verification? -Secret verification is the process of checking whether a detected secret is actually active and valid. Leakwatch ships with **54 verifiers (51 packages)** covering **84.4% of its 64 detectors (60 packages)**, making it one of the most comprehensive verification systems available under an MIT license. +Secret verification is the process of checking whether a detected secret is actually active and valid. Leakwatch ships with **65 detectors (61 packages)** and **54 registered verifier implementations (51 packages)**. Registry presence is not live capability: **39** detectors support a direct live check, **9** require trusted issuer, region, or companion context, **6** are format-only, and **11** have no verifier. -Verification is performed through two methods: -- **Live API verification** (48 detectors) -- controlled, non-destructive API calls to the service that issued the credential +Verification is classified into three methods: +- **Direct live API verification** (39 detectors) -- controlled, non-destructive API calls available in the normal production path +- **Context-required verification** (9 detectors) -- a trusted issuer, region, or paired credential is required before any safe request - **Format validation** (6 detectors) -- structural checks (decode, parse, validate format) without network calls **Why it matters:** @@ -50,8 +53,8 @@ Every finding in Leakwatch carries a verification status. Understanding these st | Status | Description | Action Required | |--------|-------------|-----------------| | `verified_active` | Secret is **valid and active** -- the provider confirmed it works | **Immediate rotation required** | -| `verified_inactive` | Secret is **invalid or revoked** -- the provider rejected it | Low priority; may still warrant cleanup | -| `unverified` | Verification was not performed (no verifier available, or `--no-verify` was used) | Manual review recommended | +| `verified_inactive` | Secret is **invalid or revoked** -- the provider gave a definitive authentication rejection under that verifier's contract | Low priority; may still warrant cleanup | +| `unverified` | Verification was not performed (no verifier, missing context, format-only capability, or `--no-verify`) | Manual review recommended | | `verify_error` | An error occurred during verification (network timeout, rate limit, etc.) | Retry or verify manually | ```mermaid @@ -68,9 +71,9 @@ stateDiagram-v2 ## 3. Verified Detectors -Leakwatch provides 54 verifiers (51 packages) across three verification types. The following table shows all verified detectors grouped by verification method. +Leakwatch provides 54 registered verifier implementations (51 packages) across three verification types. The following tables classify capability rather than equating registry count with live coverage. -### Live API Verification (48 detectors) +### Direct Live API Verification (39 detectors) These verifiers make a controlled, non-destructive API call to the provider to confirm whether the secret is active or inactive. The majority use HTTP GET; a small number (e.g., the Teams webhook verifier) use a non-destructive POST to a validation endpoint. @@ -85,9 +88,6 @@ These verifiers make a controlled, non-destructive API call to the provider to c | **AI/ML** | Anthropic API Key | `anthropic-api-key` | `api.anthropic.com/v1/models` | | **AI/ML** | Hugging Face Token | `huggingface-token` | `huggingface.co/api/whoami-v2` | | **AI/ML** | DeepSeek API Key | `deepseek-api-key` | `api.deepseek.com/models` | -| **DevTools** | GitHub PAT | `github-token` | `api.github.com/user` | -| **DevTools** | GitHub OAuth Token | `github-oauth-token` | `api.github.com/user` | -| **DevTools** | GitLab PAT | `gitlab-pat` | `{host}/api/v4/user` (defaults to `gitlab.com`; honors a co-located self-hosted instance host) | | **DevTools** | Bitbucket App Password | `bitbucket-app-password` | `api.bitbucket.org/2.0/user` | | **DevTools** | NPM Token | `npm-token` | `registry.npmjs.org/-/npm/v1/user` | | **DevTools** | PyPI Token | `pypi-api-token` | `upload.pypi.org/legacy/` | @@ -99,32 +99,44 @@ These verifiers make a controlled, non-destructive API call to the provider to c | **Communication** | Discord Bot Token | `discord-bot-token` | `discord.com/api/v10/users/@me` | | **Communication** | Telegram Bot Token | `telegram-bot-token` | `api.telegram.org/bot{token}/getMe` | | **Communication** | MS Teams Webhook | `teams-webhook` | `{webhook-url}` (non-destructive empty `{}` POST; 400 = active, 404 = inactive) | -| **Email** | SendGrid API Key | `sendgrid-api-key` | `api.sendgrid.com/v3/scopes` (needs no specific scope, so a restricted-permission key is not misread as revoked) | +| **Email** | SendGrid API Key | `sendgrid-api-key` | `api.sendgrid.com/v3/scopes` (`401` is inactive; `403`, including permission denial, remains `verify_error`) | | **Email** | Mailgun API Key | `mailgun-api-key` | `api.mailgun.net/v3/domains`, retrying `api.eu.mailgun.net/v3/domains` if the US host reports inactive (US/EU are separate tenants) | | **Email** | Postmark Server Token | `postmark-server-token` | `api.postmarkapp.com/server` | | **Payment** | Stripe Live Key | `stripe-api-key-live` | `api.stripe.com/v1/charges?limit=1` | | **Payment** | Stripe Test Key | `stripe-api-key-test` | `api.stripe.com/v1/charges?limit=1` | -| **Database** | Supabase Service Key | `supabase-service-key` | `api.supabase.com/v1/projects` (Bearer token only; no project ref or `apikey` header) | +| **DevTools** | Supabase Personal Access Token | `supabase-service-key` | Supabase Management API `api.supabase.com/v1/projects` (`sbp_` Bearer PAT; `401` is inactive, `403` remains inconclusive) | | **Infrastructure** | Databricks PAT | `databricks-token` | `{workspace-host}/api/2.0/preview/scim/v2/Me` (workspace host captured alongside the token; no host means unverified) | | **Identity** | Okta API Token | `okta-api-token` | `{domain}/api/v1/users/me` (org domain captured alongside the token) | -| **Identity** | Auth0 Management Token | `auth0-management-token` | `{tenant}/api/v2/` (tenant host decoded from the token's own `iss` JWT claim) | -| **Monitoring** | Datadog API Key | `datadog-api-key` | `api.datadoghq.com/api/v1/validate` | -| **Monitoring** | Grafana API Key | `grafana-api-key` | `grafana.com/api/user` -- self-hosted/per-stack tokens are not verifiable against this central host (no per-instance URL is captured) | | **Monitoring** | PagerDuty API Key | `pagerduty-api-key` | `api.pagerduty.com/users/me` | -| **Monitoring** | New Relic API Key | `newrelic-api-key` | `api.newrelic.com/v2/users.json` | +| **Monitoring** | New Relic user API key | `newrelic-api-key` | Read-only NerdGraph `requestContext { userId }`; fixed official US/EU endpoints with bounded fallback. Only all-region 401 is inactive; 403 and partial failures remain inconclusive. | | **Monitoring** | Sentry Auth Token | `sentry-token` | `sentry.io/api/0/` | -| **Security** | Snyk API Key | `snyk-api-key` | `api.snyk.io/rest/self` | -| **Security** | Twilio API Key | `twilio-api-key` | `api.twilio.com/2010-04-01/Accounts.json` (Basic auth paired as API Key SID + API Key Secret, not Account SID + Auth Token) | | **Secrets Mgmt** | Doppler Service Token | `doppler-token` | `api.doppler.com/v3/me` | | **Feature Flags** | LaunchDarkly SDK Key | `launchdarkly-sdk-key` | `app.launchdarkly.com/api/v2/caller-identity` | | **Code Quality** | SonarCloud Token | `sonarcloud-token` | `sonarcloud.io/api/authentication/validate` | -| **SaaS** | Shopify Access Token | `shopify-access-token` | `{shop}.myshopify.com/admin/api/2024-01/shop.json` -- requires a store domain the detector does not currently capture, so findings resolve to `unverified` until this wiring lands (see [ROADMAP](../05-ROADMAP.md)) | | **SaaS** | Notion Token | `notion-token` | `api.notion.com/v1/users/me` | | **SaaS** | Linear API Key | `linear-api-key` | `api.linear.app/graphql` | | **SaaS** | Figma PAT | `figma-pat` | `api.figma.com/v1/me` | | **SaaS** | Airtable PAT | `airtable-pat` | `api.airtable.com/v0/meta/whoami` | | **Blockchain** | Infura API Key | `infura-api-key` | `mainnet.infura.io/v3/{key}` | +### Requires Trusted or Companion Context (9 detectors) + +These implementations are registered, but a bare detector finding cannot safely select the issuer or authenticate the verification request. Missing context produces `unverified` without a network request. + +Use the repeatable command-line-only form `--verifier-origin detector-id=https://host` to provide trusted origins. Project configuration and environment variables cannot set these targets. `--grafana-instance-url` remains a backward-compatible Grafana alias. + +| Detector | Detector ID | Required context and behavior | +|----------|-------------|-------------------------------| +| Auth0 Management Token | `auth0-management-token` | Operator-trusted Auth0 tenant/custom HTTPS origin; configure with `--verifier-origin auth0-management-token=https://tenant`. JWT claims from scanned content never select a request target; only `401` on the trusted origin is inactive | +| Grafana service-account token | `grafana-api-key` | Trusted HTTPS instance origin from `--grafana-instance-url`; repository content cannot choose the target, and `401` is inactive only on that trusted issuer | +| Twilio API Key Secret | `twilio-api-key` | The detector treats the secret as opaque and reports it only with a one-to-one nearby `SK...` SID; bare SIDs are not findings. Configure the trusted US1/IE1/AU1 origin explicitly; `403` is permission-ambiguous and never inactive | +| Shopify Access Token | `shopify-access-token` | Configure the operator-trusted `*.myshopify.com` store origin explicitly. Finding-controlled domains are ignored; the verifier uses the pinned 2026-07 Admin GraphQL shop identity query | +| GitHub PAT | `github-token` | Configure a trusted GitHub.com or GHES API origin explicitly. Both issuers use `ghp_`/`github_pat_`; repository metadata never chooses the issuer. Safe scope/count and expiry headers enrich active findings when present | +| GitHub OAuth/App Token | `github-oauth-token` | Trusted GitHub.com or GHES API origin; `gho_`/`ghu_` use `/user` with safe scope/expiry enrichment, `ghs_` uses `/installation/repositories`, and side-effectful `ghr_` refresh-token exchange is never attempted | +| GitLab PAT | `gitlab-pat` | Configure an operator-trusted GitLab.com or self-managed HTTPS origin. Only `glpat-` uses `/api/v4/user`; after active proof a best-effort self-metadata request adds bounded, validated standard or GitLab 19.2+ granular scopes and expiry. Other recognized subtypes remain `unverified`. Only GitLab's standard invalid-token JSON `401` is inactive; DPoP challenges are inconclusive | +| Datadog API Key | `datadog-api-key` | Configure one of the exact official US1/US3/US5/EU/AP1/AP2/UK1/US1-FED/US2-FED API origins; arbitrary hosts are rejected | +| Snyk API Key | `snyk-api-key` | Configure the trusted regional, government, or private API origin; only `401` is inactive, while permission/plan `403` remains `verify_error` | + ### Format Validation (6 detectors) These verifiers perform structural validation without making network calls. They check format and decode tokens without contacting the provider. @@ -138,7 +150,7 @@ These verifiers perform structural validation without making network calls. They | Snowflake Credentials | `snowflake-credentials` | Connection string format and credential structure validation | | Coinbase API Key | `coinbase-api-key` | Key-shape validation only; live verification needs HMAC-SHA256 request signing with the paired secret, which the detector cannot reliably supply, so the result is always `unverified` -- never a false active/inactive | -### Not Verifiable (10 detectors) +### Not Verifiable (11 detectors) These detectors have no registered verifier, so their findings are always reported as `unverified`. @@ -154,6 +166,7 @@ These detectors have no registered verifier, so their findings are always report | LDAP Credentials | `ldap-credentials` | Requires direct connection to LDAP directory server | | Slack Webhook | `slack-webhook` | Verification would send a message (side effect) | | Discord Webhook URL | `discord-webhook-url` | Verification would post a message (side effect) | +| Structured Configuration Secret | `structured-config-secret` | Field context can identify a secret role but cannot determine the credential provider or issuer | --- @@ -218,12 +231,14 @@ If the Secret Access Key is not found alongside the Access Key ID, verification ### How It Works -The GitHub verifier sends a `GET /user` request to the GitHub API using the discovered token as a Bearer token. This is a read-only call that returns information about the authenticated user. +GitHub.com and GitHub Enterprise Server (GHES) issue credentials with the same token prefixes. A token alone therefore cannot identify its issuer. Leakwatch sends a `GET /user` request only after a trusted GitHub API origin has been selected with the command-line-only `--verifier-origin` flag. Without that explicit origin, GitHub token findings return `unverified` without making a network request. -- **Detector ID:** `github-token` -- **API endpoint:** `https://api.github.com/user` +- **Detector IDs:** `github-token`, `github-oauth-token` +- **API endpoints:** trusted GitHub.com or GHES API origin plus `/user` for PAT/`gho_`/`ghu_`; `/installation/repositories` for `ghs_` - **Headers:** `Authorization: Bearer `, `User-Agent: leakwatch-verifier` +This fail-closed behavior prevents a valid GHES token from being sent to GitHub.com and incorrectly reported as inactive. Refresh tokens (`ghr_`) are detected but never exchanged because exchange rotates them; they remain `unverified` even when an issuer is trusted. Only a strict authentication rejection from the selected issuer and subtype-appropriate endpoint may produce `verified_inactive`. + ### What It Reveals When the token is active, the verifier returns: @@ -236,8 +251,9 @@ When the token is active, the verifier returns: | HTTP Status | Verification Result | |-------------|-------------------| -| `200 OK` | `verified_active` -- token is valid | -| `401 Unauthorized` | `verified_inactive` -- token is invalid or revoked | +| No trusted API origin | `unverified` -- no request is made | +| `200 OK` from trusted origin | `verified_active` -- token is valid | +| `401 Unauthorized` from trusted origin | `verified_inactive` -- token is invalid or revoked for that issuer | | Other | `verify_error` -- unexpected response | ### Example Output @@ -291,16 +307,17 @@ The verification engine manages API calls carefully to avoid overwhelming provid |-----------|---------|-------------| | Concurrency | 4 workers | Number of parallel verification goroutines | | Rate limit | 10 req/sec | Maximum verification requests per second (token bucket) | -| Timeout | 10 seconds | Per-request timeout for each verification API call | +| Timeout | 10 seconds | Per-finding timeout for the complete verification operation, including bounded provider-region fallback | ### How It Works The verification engine uses a worker pool pattern with a shared rate limiter: 1. **Worker pool** -- A fixed number of goroutines (default 4) process verification jobs concurrently. -2. **Token bucket rate limiter** -- Before each API call, the worker acquires a token from a `golang.org/x/time/rate` limiter. If the bucket is empty, the worker waits until a token becomes available. -3. **Per-request timeout** -- Each verification call has its own context timeout (default 10s). If the provider does not respond in time, the finding is marked `verify_error`. +2. **Global + provider token buckets** -- Immediately before each actual API call, the worker acquires admission from both the global ceiling and the finding's provider/detector bucket. Format-only and missing-context paths consume no token. The detector bucket limits that provider's own send rate and the shared global bucket bounds aggregate traffic; the current single configured rate does not guarantee fair scheduling between providers. +3. **Per-finding timeout** -- Each finding's complete verification operation has one context timeout (default 10s), including bounded provider-region fallback. If the provider does not respond in time, the finding is marked `verify_error`. 4. **Context cancellation** -- If the parent context is cancelled (e.g., the user presses Ctrl+C), all pending verifications are abandoned gracefully. +5. **Bounded HTTP 429 recovery** -- Safe GET/HEAD probes may retry once when the provider supplies a valid `Retry-After` no greater than 2 seconds. The delay includes bounded jitter; the retry passes through both limiter buckets again and never exceeds the per-finding context deadline. POST and other potentially unsafe methods, missing/invalid headers, and longer requested waits are not retried; they remain `verify_error`. ### Configuration @@ -320,7 +337,7 @@ verification: Verification involves sending discovered credentials to provider APIs. Keep the following in mind: -- **Credentials are transmitted over the network** -- The raw secret value is sent to the provider's API endpoint (e.g., `sts.amazonaws.com`, `api.github.com`) over HTTPS. Ensure your network allows outbound HTTPS traffic to these endpoints. +- **Credentials are transmitted over the network** -- The raw secret value is sent only to the verifier's fixed provider endpoint, credential-derived endpoint, or explicitly trusted operator-selected origin over HTTPS. Context-dependent verifiers make no request when a trustworthy destination is unavailable. - **Leakwatch never logs raw secrets** -- Verifiers are designed to never log, persist, or cache the raw credential values. Only redacted values appear in logs. - **Non-destructive operations only** -- All verification calls are controlled and non-destructive. The majority use HTTP GET; where a POST is required (e.g., the Teams webhook verifier), it targets a validation endpoint and does not cause state changes or side effects. - **Network requirements** -- Verification requires outbound HTTPS access. In air-gapped or restricted environments, use `--no-verify` to skip verification entirely. diff --git a/docs/guides/slack-scanning.md b/docs/guides/slack-scanning.md index ec83e35..da3d17a 100644 --- a/docs/guides/slack-scanning.md +++ b/docs/guides/slack-scanning.md @@ -1,11 +1,13 @@ # Leakwatch - Slack Workspace Scanning Guide -> **Document Version:** 1.0 -> **Date:** 2026-03-24 +> **Document Version:** 1.1 +> **Date:** 2026-08-11 > **Status:** Approved --- +> **Documentation role:** Supplemental Slack operations deep dive. The [Slack user manual](../user-manuals/en/scanning/slack.md) is authoritative for current flags and supported content. + ## Table of Contents 1. [Why Scan Slack?](#1-why-scan-slack) @@ -62,7 +64,9 @@ To scan a Slack workspace, you need a Slack Bot Token (`xoxb-...`) with the appr | `channels:read` | List public channels | | `groups:history` | Read messages in private channels the bot is a member of | | `groups:read` | List private channels the bot is a member of | +| `im:read` | List direct-message conversations (only if `--include-dms` is used) | | `im:history` | Read direct messages (only if `--include-dms` is used) | +| `mpim:read` | List group direct-message conversations (only if `--include-dms` is used) | | `mpim:history` | Read group direct messages (only if `--include-dms` is used) | | `files:read` | Read uploaded file content (only if `--include-files` is used) | @@ -170,19 +174,19 @@ By default, Leakwatch scans only public and private channels. To include direct leakwatch scan slack --include-dms ``` -> **Privacy note:** Scanning direct messages has significant privacy implications. Ensure you have organizational approval and that your employees are aware that DMs may be scanned for security purposes. Many organizations restrict DM scanning to incident response scenarios or require explicit consent. The bot's OAuth scopes `im:history` and `mpim:history` must be granted for this feature to work. +> **Privacy note:** Scanning direct messages has significant privacy implications. Ensure you have organizational approval and that your employees are aware that DMs may be scanned for security purposes. Many organizations restrict DM scanning to incident response scenarios or require explicit consent. The bot's OAuth scopes `im:read`, `im:history`, `mpim:read`, and `mpim:history` must be granted for this feature to work. --- ## 7. File Scanning -> **Not yet implemented — planned for a future release.** - -File scanning is **not currently implemented**. The current `scan slack` command scans only message text (channel messages, DMs if `--include-dms` is used). Uploaded file content is not fetched or scanned. +File scanning is opt-in. Add `--include-files` and grant the bot `files:read` to download and scan text-like attachments: -The `--include-files` flag is accepted by the CLI for forward-compatibility but has **no effect** — it is a no-op in the current release. The `files:read` OAuth scope listed in §2.2 is required only when file scanning is eventually enabled. +```bash +leakwatch scan slack --include-files --max-file-size 5242880 +``` -File scanning (downloading and scanning the content of files uploaded to Slack channels — configuration files, scripts, logs, etc.) is a planned capability. See the [Roadmap "Documented-but-Unimplemented Gaps" §1](../05-ROADMAP.md#master-review--documented-but-unimplemented-gaps) for tracking status. +Leakwatch only sends the bot token to Slack-owned HTTPS download URLs. File metadata and downloads use separate limiter buckets and bounded 429 retry policies without consuming each other's or history capacity. Attachment chunks transfer with direct backpressure; content that exceeds `--max-file-size` or fails the conservative UTF-8 text classifier is skipped even when MIME metadata is empty or spoofed. The same Slack file ID is downloaded at most once per scan. --- @@ -190,31 +194,26 @@ File scanning (downloading and scanning the content of files uploaded to Slack c ### 8.1 Slack API Rate Limits -The Slack API enforces rate limits organized into tiers: - -| Tier | Limit | Affected Methods | -|------|-------|------------------| -| **Tier 1** | 1 request per minute | Rarely used | -| **Tier 2** | 20 requests per minute | `conversations.list` | -| **Tier 3** | 50 requests per minute | `conversations.history` | -| **Tier 4** | 100 requests per minute | Most read methods | +Slack assigns limits per method and app distribution model. Its current +`conversations.history` contract is Tier 3 for Marketplace and internal +customer-built apps, but one request per minute with a 15-item page limit for +new commercially distributed non-Marketplace apps. Consult Slack's current +method documentation for your app rather than assuming one workspace-wide +tier. ### 8.2 Configuring the Rate Limit -The `--rate-limit` flag controls the maximum number of Slack API requests per second. The default is 20 requests per second: +Leakwatch keeps independent operation buckets because Slack applies Web API limits per method/workspace/app. Defaults are one request/minute for the special `conversations.history` profile, Tier 2 (20+/minute) for `conversations.list`, Tier 4 (100+/minute) for `files.info`, and a separate conservative 100/minute client-side cap for attachment downloads. The `--rate-limit` flag explicitly replaces every bucket with one common **per-operation** requests-per-second ceiling: ```bash -# Default rate (20 req/s) +# Default rate (one request/minute) leakwatch scan slack -# Conservative rate for shared workspaces -leakwatch scan slack --rate-limit 5 - -# Higher rate if your Slack plan allows it -leakwatch scan slack --rate-limit 50 +# Higher rate for a Marketplace/internal app with Tier 3 history access +leakwatch scan slack --rate-limit 0.8 ``` -> **Recommendation:** If you encounter rate-limiting errors (`slack_rate_limited`), reduce the rate limit. For Enterprise Grid workspaces with higher API quotas, you may safely increase it. +Leakwatch also honors Slack's `Retry-After` response and performs bounded retries. Raising the local cap does not override Slack's server-side contract; reduce it if you still receive rate limits. --- @@ -337,7 +336,10 @@ flowchart TD L -->|"No"| N["Process all messages"] M --> N - N --> R["Secret Detection Engine\n(message text only; file scanning not yet implemented)"] + N --> O{"--include-files?"} + O -->|"Yes"| P["Fetch bounded text attachments"] + O -->|"No"| R + P --> R["Secret Detection Engine"] R --> S["Verification"] S --> T["Apply Severity Filter"] @@ -369,7 +371,7 @@ flowchart TD | Problem | Cause | Solution | |---------|-------|----------| -| `slack_rate_limited` errors in logs | API rate limit exceeded | Lower `--rate-limit` (e.g., `--rate-limit 5`) | +| `slack_rate_limited` errors in logs | API rate limit exceeded | Use `--rate-limit 0` to restore Slack-safe operation defaults, or reduce an explicit positive override | | Scan is very slow | Rate limit set too low | Increase `--rate-limit` if your Slack plan allows higher quotas | | Intermittent timeouts | Network instability or Slack API issues | Retry the scan; consider running from a stable network | diff --git a/docs/guides/vscode-extension.md b/docs/guides/vscode-extension.md index 3749496..91338e4 100644 --- a/docs/guides/vscode-extension.md +++ b/docs/guides/vscode-extension.md @@ -1,11 +1,13 @@ # Leakwatch - VS Code Extension Guide -> **Document Version:** 1.0 -> **Date:** 2026-03-24 +> **Document Version:** 1.1 +> **Date:** 2026-08-11 > **Status:** Approved --- +> **Documentation role:** Supplemental extension deep dive. The [user manual](../user-manuals/en/getting-started/introduction.md) is authoritative for Leakwatch CLI behavior; this guide owns only extension-specific workflows. + ## 1. Overview The Leakwatch VS Code extension brings secret detection directly into your editor. It runs the Leakwatch CLI under the hood and surfaces findings as native VS Code diagnostics, so you can catch leaked secrets without leaving your workflow. @@ -31,9 +33,21 @@ Verify the CLI is available: leakwatch version ``` -### 2.2 Install from Marketplace / GitHub Releases (Not Yet Available) +### 2.2 Install a CI-built VSIX + +The extension is not yet advertised as generally available in the Visual Studio Marketplace. Extension CI does, however, lint, test, package, and upload a reviewable `leakwatch-vsix` workflow artifact. Download the artifact from a trusted workflow run and install it with `code --install-extension leakwatch-.vsix`. + +Marketplace publication is experimental and deliberately separate from ordinary CI. `.github/workflows/vscode-release.yml` accepts `publish=true` only from `main`, publishes the exact VSIX produced by its package job, and reads `VSCE_PAT` only from the `vscode-marketplace` environment. Tags do not publish automatically. -The extension is not yet published to the VS Code Marketplace, and no CI workflow packages or uploads a `.vsix` to GitHub Releases today — `.github/workflows/vscode-ci.yml` only lints, compiles, and tests `vscode/` on every push/PR; it does not run `vsce package` or attach an artifact. Until one of those ships, build the `.vsix` yourself with [§2.3 Build from Source](#23-build-from-source) below. +Repository administrators must configure that environment before the first publication: + +1. Add at least one required reviewer and enable **Prevent self-review**. +2. Set deployment branches to **Protected branches only** and keep `main` protected. +3. Store `VSCE_PAT` as an environment secret, never as a repository secret. +4. Create a short-lived fine-grained token named `VSCODE_ENV_AUDIT_TOKEN` as an environment secret. Restrict it to this repository and grant only **Environments: read** so the workflow can prove that the environment-scoped `VSCE_PAT` metadata exists. +5. Audit these settings and rotate both tokens after ownership or branch-protection changes. + +The publish job queries GitHub's environment and environment-secret metadata APIs. It fails before loading the VSIX or Marketplace PAT unless the required-reviewer, self-review, and protected-branch policies are present and `VSCE_PAT` actually exists in the protected environment. `VSCODE_ENV_AUDIT_TOKEN` is read-only and is never sent to the Marketplace; `VSCE_PAT` is never placed in process arguments. The environment approval itself remains an external GitHub control; this runbook and the executable audit make that dependency explicit rather than assuming an environment name is sufficient. ### 2.3 Build from Source @@ -42,14 +56,11 @@ The extension is not yet published to the VS Code Marketplace, and no CI workflo git clone https://github.com/HodeTech/Leakwatch.git cd Leakwatch/vscode -# Install dependencies -npm install - -# Build for production -npm run compile +# Install the lockfile-pinned dependency graph +npm ci # Package as VSIX -npx @vscode/vsce package +npm run package:vsix # Install the generated VSIX code --install-extension leakwatch-0.1.0.vsix @@ -96,7 +107,7 @@ All settings are under the `leakwatch.*` namespace. Open **Settings** (`Ctrl+,`) **`leakwatch.minSeverity`** -- Controls the noise level. Setting this to `high` hides medium and low findings, which is useful in large projects where generic API key detections may be noisy. -**`leakwatch.customRulesPath`** -- Points to a YAML file containing custom detection rules. See [Custom Rules Integration](#8-custom-rules-integration) below. +**`leakwatch.customRulesPath`** -- Points to a YAML file containing custom detection rules. See [Custom Rules Integration](#9-custom-rules-integration) below. --- diff --git a/docs/standards/00-DOCUMENTATION-STANDARDS.md b/docs/standards/00-DOCUMENTATION-STANDARDS.md index 0b5ca5e..28bba21 100644 --- a/docs/standards/00-DOCUMENTATION-STANDARDS.md +++ b/docs/standards/00-DOCUMENTATION-STANDARDS.md @@ -1,14 +1,14 @@ # Leakwatch - Documentation Standards -> **Document Version:** 1.0 -> **Date:** 2026-03-24 +> **Document Version:** 2.0 +> **Date:** 2026-08-11 > **Status:** Approved --- ## 1. General Principles -1. **Language:** All documents must be written in English. Technical terms (interface, pipeline, chunk, etc.) remain in English. +1. **Language:** English is the authoritative source language. The Turkish user manual is a reviewed translation that must preserve the English page set and product semantics. Code comments, logs, errors, architecture documents, standards, ADRs, and supplemental guides remain English. 2. **Format:** All documents are written in GitHub-Flavored Markdown (GFM) format. 3. **Encoding:** UTF-8, line endings LF (`\n`). 4. **Line length:** No mandatory line length limit in Markdown files; use natural paragraph flow. @@ -33,14 +33,19 @@ docs/ │ ├── 01-CODE-REVIEW-STANDARDS.md │ ├── 02-RELEASE-STANDARDS.md │ └── 04-DEVELOPMENT-STANDARDS.md -├── 05-ROADMAP.md # Roadmap (under root docs/) -└── guides/ # Usage guides - ├── getting-started.md - ├── configuration.md - ├── ci-cd-integration.md - ├── custom-rules.md - ├── container-scanning.md - └── cloud-scanning.md +├── user-manuals/ # Canonical product behavior (EN source + TR translation) +│ ├── _meta.yaml # Canonical page/language navigation contract +│ ├── en/ # Authoritative user-facing source +│ └── tr/ # Reviewed translation; same page set as EN +├── guides/ # Supplemental operational and implementation deep dives +│ ├── README.md # Scope and canonical-source boundary +│ └── *.md # Supplemental deep dives +└── 05-ROADMAP.md # Roadmap (under root docs/) + +site/js/manuals/ # Generated publication output; never edited by hand +├── _index.js # Navigation bundle from _meta.yaml +├── en.js # Compiled English manual +└── tr.js # Compiled Turkish manual ``` ### 2.1 Directory Responsibilities @@ -50,9 +55,36 @@ docs/ | `architecture/` | Architecture decisions, technical design, competitive analysis | Developer, architect | | `decisions/` | ADR — context and rationale of architecture decisions | Developer, architect | | `standards/` | Coding, testing, documentation, CI/CD standards | Developer, contributor | -| `guides/` | Installation, usage, integration guides | End user | +| `user-manuals/en/` | Current CLI, configuration, detector, verifier, output, and integration behavior | End user; authoritative | +| `user-manuals/tr/` | Turkish translation of the English manual with identical navigation/page coverage | End user; translated | +| `guides/` | Supplemental deep dives, operational rationale, and implementation detail; must defer current product contracts to the user manual | Advanced user, operator, contributor | | Root `docs/` | Roadmap, general documents | Everyone | +### 2.2 Canonical and Generated Boundaries + +The documentation publication chain is intentionally one-way: + +```mermaid +flowchart LR + EN["docs/user-manuals/en\nAuthoritative behavior"] --> TR["docs/user-manuals/tr\nReviewed translation"] + META["docs/user-manuals/_meta.yaml\nNavigation contract"] --> BUILD["tools/site-build"] + EN --> BUILD + TR --> BUILD + BUILD --> SITE["site/js/manuals/*.js\nGenerated; do not edit"] +``` + +- Product behavior changes start in code and executable contracts, then update the English user manual. +- The matching Turkish page is updated in the same change. `_meta.yaml` requires identical page coverage for every declared language. +- `tools/site-build` compiles both languages into `site/js/manuals/*.js`. Generated bundles must never be edited by hand and CI rejects generated drift. +- Supplemental guides may explain advanced workflows or rationale, but they must link to the user manual for current flags, defaults, statuses, counts, and support claims. If the two disagree, the user manual and executable contract win. +- Architecture documents describe implementation. They do not redefine user-facing CLI or configuration contracts. + +### 2.3 Release-Version Examples + +- An example advertised as current must use `internal/meta.ReleaseVersion` (or a floating supported pin such as `@v1` or `:latest`). +- An intentionally historical example must be associated with `` in the same paragraph, table block, or fenced example and explain why that exact older value matters. +- CI scans Leakwatch-specific full-version pins across current Markdown surfaces, including root policies, contributor material, architecture, ADRs, guides, user manuals, site documentation, editor documentation, and workflow documentation. Inherently historical inventories such as the changelog and roadmap are excluded. An unmarked stale pin fails the documentation contract test. + --- ## 3. Document Template @@ -110,7 +142,7 @@ Related ADRs (if any). - File name: `ADR-NNNN-short-title.md` (lowercase, hyphen-separated) - Sequence number is 4 digits, zero-padded: `0001`, `0002`, ... - Each ADR is added to the `docs/decisions/README.md` index -- Accepted ADRs are not modified — to override, write a new ADR and mark the old one as "Deprecated" +- Accepted ADR decisions are not rewritten — record a partial change in a new ADR and mark the old ADR "Amended"; replace an entire decision only with a new ADR that marks the old one "Superseded" - ADRs are also added to the `CLAUDE.md` reference table --- @@ -283,3 +315,7 @@ The following checklist must be completed before opening a document PR: - [ ] Tables do not exceed 6 columns - [ ] Spelling has been checked - [ ] Mermaid diagrams render correctly on GitHub (preview) +- [ ] English behavior changes have matching Turkish updates +- [ ] Generated site bundles were rebuilt and have no unexplained diff +- [ ] Leakwatch-specific release pins are current or explicitly historical +- [ ] Relative Markdown links resolve to tracked files diff --git a/docs/standards/05-PROVIDER-CONTRACT-AUDITS.md b/docs/standards/05-PROVIDER-CONTRACT-AUDITS.md new file mode 100644 index 0000000..d08f29a --- /dev/null +++ b/docs/standards/05-PROVIDER-CONTRACT-AUDITS.md @@ -0,0 +1,38 @@ +# Provider Verification Contract Audits + +Provider APIs change independently of Leakwatch. A verifier may therefore keep compiling while its endpoint, region model, status semantics, response schema, or credential type becomes stale. This standard makes reviewed contracts explicit and time-bounded without pretending that every registered verifier has already received a current audit. + +## Canonical evidence + +`internal/meta.VerificationCapability` is the machine-readable source of truth. A completed audit must set both: + +- `LastContractReviewedAt`, using `YYYY-MM-DD`; and +- one or more `ContractReferenceURLs` that point to primary provider documentation. + +An empty date means **not audited under this process**. It must never be presented as a completed review. A date without primary references, references without a date, non-HTTPS or non-allowlisted provider hosts, future dates, and reviewed contracts older than 180 days fail the metadata contract tests. + +## Audit procedure + +For every audited verifier: + +1. Confirm the credential class and supported token subtypes. +2. Confirm the endpoint, HTTP method, authentication scheme, provider regions, and any trusted issuer/origin requirement. +3. Confirm the exact evidence that means active, inactive, or verification error. Permission failures and malformed success responses must remain fail-conservative. +4. Confirm bounded request count, redirects, response size/content type, cancellation, rate limiting, and secret-safe errors/logs. +5. Update the verifier's adversarial response matrix before advancing the review date. +6. Record only primary provider documentation URLs in the capability manifest. + +The weekly `provider-contract-audit.yml` workflow runs the freshness gate and a bounded live documentation check. Every recorded URL must resolve with a 2xx response on an allowlisted official provider host; a recorded fragment must exist in the returned page. Redirects are bounded and may only remain within the official-host allowlist. Its purpose is to make audit due dates and dead/misdirected evidence visible and blocking; changing only the date without reviewing the references, implementation, and tests violates this standard. + +## Live canaries + +Default CI must never contain or transmit real credentials. Provider canaries remain **planned** until a provider-specific design proves all of the following: + +- explicit opt-in in a protected environment; +- a dedicated least-privilege test credential owned by the project; +- no billing, mutation, notification, rotation, or third-party alert side effect; +- secret masking in every process, error, log, artifact, and cache boundary; +- bounded requests, timeout, rate limit, and immediate revocation procedure; and +- a documented provider owner and audit trail. + +Synthetic `httptest` contract matrices remain the required CI mechanism. A live canary may supplement them but can never replace them. diff --git a/docs/user-manuals/_meta.yaml b/docs/user-manuals/_meta.yaml index 92a6d6e..a46d8cb 100644 --- a/docs/user-manuals/_meta.yaml +++ b/docs/user-manuals/_meta.yaml @@ -1,12 +1,15 @@ # Leakwatch user-manual navigation metadata. # # This file is the single source of truth for the documentation structure. +# English pages are authoritative for current product behavior. Turkish pages +# are reviewed translations and MUST preserve the same page set and semantics. # The site-build tool (tools/site-build) reads it to: # 1. discover which Markdown pages to compile, and in what order, and # 2. generate the navigation tree rendered by the website sidebar. # # Markdown source lives at: docs/user-manuals///.md # Every page listed below MUST exist for every language in `languages`. +# site/js/manuals/*.js is generated from these sources; never edit it by hand. languages: [en, tr] default_language: en diff --git a/docs/user-manuals/en/ci-cd/docker-usage.md b/docs/user-manuals/en/ci-cd/docker-usage.md index 42ced87..4091982 100644 --- a/docs/user-manuals/en/ci-cd/docker-usage.md +++ b/docs/user-manuals/en/ci-cd/docker-usage.md @@ -16,8 +16,8 @@ ghcr.io/hodetech/leakwatch | Tag | Description | |-----|-------------| | `:latest` | Most recent release | -| `:v1.5.0` | Exact version pin | -| `:v1.5` | Minor-version pin (tracks patch releases) | +| `:v1.8.0` | Exact current-release pin | +| `:v1.7` | Current minor-version pin (tracks patch releases) | The image is based on Alpine, runs as the non-root user `leakwatch`, uses `/scan` as the working directory, and has `leakwatch` as its entrypoint. diff --git a/docs/user-manuals/en/ci-cd/github-action.md b/docs/user-manuals/en/ci-cd/github-action.md index d2d41d4..3eb5694 100644 --- a/docs/user-manuals/en/ci-cd/github-action.md +++ b/docs/user-manuals/en/ci-cd/github-action.md @@ -88,7 +88,7 @@ SARIF upload requires the job to declare `permissions: security-events: write`. | `working-directory` | `.` | Directory to run the scan from. | | `sarif-upload` | `false` | Upload SARIF results to GitHub Code Scanning after the scan. | | `fail-on-findings` | `true` | Fail the workflow step when findings are reported (exit code 1). When `false`, a `::warning::` annotation is emitted instead so the scan does not block the pipeline. Hard errors (exit code ≥ 2) always fail the step regardless of this setting. | -| `version` | `latest` | Leakwatch version to install: `latest`, or a release tag such as `v1.5.0` to pin a specific release. | +| `version` | `latest` | Leakwatch version to install: `latest`, or a release tag such as `v1.8.0` to pin a specific release. | | `release-repo` | `HodeTech/Leakwatch` | Repository (`owner/name`) to download the release binary from. Override only for forks or self-hosted mirrors. | ## Outputs @@ -144,10 +144,10 @@ For reproducible builds, pin `version` to a specific tag: ```yaml - uses: HodeTech/Leakwatch@v1 with: - version: "v1.5.0" + version: "v1.8.0" ``` -This downloads the prebuilt `v1.5.0` binary from the [Leakwatch releases](https://github.com/HodeTech/Leakwatch/releases) and verifies its SHA-256 checksum before running. For maximum supply-chain safety you can also pin the action itself to a commit SHA, e.g. `uses: HodeTech/Leakwatch@`. +This downloads the prebuilt `v1.8.0` binary from the [Leakwatch releases](https://github.com/HodeTech/Leakwatch/releases) and verifies its SHA-256 checksum before running. For maximum supply-chain safety you can also pin the action itself to a commit SHA, e.g. `uses: HodeTech/Leakwatch@`. ## Scanning only changed code (pull-request diff) diff --git a/docs/user-manuals/en/ci-cd/other-ci.md b/docs/user-manuals/en/ci-cd/other-ci.md index c66456a..122fe94 100644 --- a/docs/user-manuals/en/ci-cd/other-ci.md +++ b/docs/user-manuals/en/ci-cd/other-ci.md @@ -20,7 +20,7 @@ go install github.com/HodeTech/leakwatch@latest Pin to a specific version for reproducible builds: ```bash -go install github.com/HodeTech/leakwatch@v1.5.0 +go install github.com/HodeTech/leakwatch@v1.8.0 ``` ### via the Docker image (no Go required) @@ -76,7 +76,7 @@ leakwatch: stage: test image: golang:1.25-alpine script: - - go install github.com/HodeTech/leakwatch@v1.5.0 + - go install github.com/HodeTech/leakwatch@v1.8.0 - leakwatch scan fs . --format json -o leakwatch.json --no-verify artifacts: when: always @@ -108,7 +108,7 @@ leakwatch scan fs . --no-verify --format sarif -o results.sarif ```yaml - script: | - go install github.com/HodeTech/leakwatch@v1.5.0 + go install github.com/HodeTech/leakwatch@v1.8.0 leakwatch scan fs . --format sarif -o $(Build.ArtifactStagingDirectory)/leakwatch.sarif --no-verify displayName: "Leakwatch secret scan" @@ -124,7 +124,7 @@ leakwatch scan fs . --no-verify --format sarif -o results.sarif stage('Secret scan') { steps { sh ''' - go install github.com/HodeTech/leakwatch@v1.5.0 + go install github.com/HodeTech/leakwatch@v1.8.0 leakwatch scan fs . --format json -o leakwatch.json --no-verify ''' archiveArtifacts artifacts: 'leakwatch.json', allowEmptyArchive: true diff --git a/docs/user-manuals/en/ci-cd/pre-commit.md b/docs/user-manuals/en/ci-cd/pre-commit.md index 9d0e170..9315aa9 100644 --- a/docs/user-manuals/en/ci-cd/pre-commit.md +++ b/docs/user-manuals/en/ci-cd/pre-commit.md @@ -22,7 +22,7 @@ Add a `.pre-commit-config.yaml` file to the root of your repository (or extend a ```yaml repos: - repo: https://github.com/HodeTech/Leakwatch - rev: v1.5.0 + rev: v1.8.0 hooks: - id: leakwatch ``` @@ -56,7 +56,7 @@ The hook's default behavior matches `leakwatch scan fs` with no additional flags ```yaml repos: - repo: https://github.com/HodeTech/Leakwatch - rev: v1.5.0 + rev: v1.8.0 hooks: - id: leakwatch args: @@ -105,7 +105,7 @@ Using `SKIP=leakwatch` bypasses all secret scanning for that commit. Use it only Pin `rev:` to a specific tag rather than a branch name. This ensures all developers on the team use the same detector set and the hook does not silently upgrade mid-sprint: ```yaml -rev: v1.5.0 # pin; do not use 'main' or 'HEAD' +rev: v1.8.0 # pin; do not use 'main' or 'HEAD' ``` Update by running: diff --git a/docs/user-manuals/en/configuration/config-file.md b/docs/user-manuals/en/configuration/config-file.md index 42f2a97..d4287eb 100644 --- a/docs/user-manuals/en/configuration/config-file.md +++ b/docs/user-manuals/en/configuration/config-file.md @@ -99,7 +99,8 @@ verification: # Enable live verification against provider APIs. enabled: true - # Per-request HTTP timeout. Must be >= 1ms when verification is enabled. + # Per-finding verification-operation timeout, including bounded fallback and + # request admission. Must be >= 1ms when verification is enabled. # Use a duration string (e.g. "10s", "500ms") — a bare integer is # treated as nanoseconds and will fail validation. timeout: 10s @@ -176,7 +177,7 @@ custom-rules: [] ``` :::note -`detection.entropy.threshold` controls which entropy value is displayed alongside a finding, and gates any detector that opts into entropy-based heuristics — currently only the built-in `generic-api-key` detector, plus custom rules that declare their own `entropy` field. A match from one of these whose entropy falls below the threshold is suppressed as a likely placeholder. Every other (structural/format-anchored) built-in detector — `aws-access-key-id`, `github-token`, and the rest — has its own fixed match criteria and is **never** dropped by this setting, regardless of entropy. +`detection.entropy.enabled` controls whether a computed entropy value is present alongside findings. `detection.entropy.threshold` does not control display; it gates detectors that opt into engine entropy heuristics — currently only the built-in `generic-api-key` detector. A match whose entropy falls below that threshold is suppressed as a likely placeholder. Custom rules that declare an `entropy` field apply their own independent per-rule threshold. Every structural/format-anchored built-in detector — `aws-access-key-id`, `github-token`, and the rest — is **never** dropped by the engine threshold, regardless of entropy. ::: ## Validation diff --git a/docs/user-manuals/en/detectors/custom-rules.md b/docs/user-manuals/en/detectors/custom-rules.md index 64835fe..0a4de1a 100644 --- a/docs/user-manuals/en/detectors/custom-rules.md +++ b/docs/user-manuals/en/detectors/custom-rules.md @@ -1,11 +1,11 @@ --- title: "Custom Rules" -description: "How to define your own secret detection patterns in YAML and add them to a Leakwatch scan alongside the 64 built-in detectors." +description: "How to define your own secret detection patterns in YAML and add them to a Leakwatch scan alongside the 65 built-in detectors." --- # Custom Rules -The 64 built-in detectors cover widely used credential formats, but every organisation has internal tokens, proprietary service keys, or environment-specific patterns that no generic tool can anticipate. Custom rules let you extend Leakwatch with your own patterns — defined in plain YAML, loaded at runtime — without modifying source code or rebuilding the binary. +The 65 built-in detectors cover widely used credential formats, but every organisation has internal tokens, proprietary service keys, or environment-specific patterns that no generic tool can anticipate. Custom rules let you extend Leakwatch with your own patterns — defined in plain YAML, loaded at runtime — without modifying source code or rebuilding the binary. ## Where custom rules live @@ -116,5 +116,5 @@ filter: ## See also - [Configuration: Config File](#/configuration/config-file) — full reference for `.leakwatch.yaml`, including where `custom-rules:` sits in the document structure. -- [Detector Catalog](#/detectors/detector-catalog) — the 64 built-in detectors, to check for ID conflicts before naming your custom rule. +- [Detector Catalog](#/detectors/detector-catalog) — the 65 built-in detectors, to check for ID conflicts before naming your custom rule. - [How It Works](#/getting-started/how-it-works) — the Aho-Corasick pre-filter pipeline that `keywords` plugs into. diff --git a/docs/user-manuals/en/detectors/detector-catalog.md b/docs/user-manuals/en/detectors/detector-catalog.md index 98a72ba..decb2a9 100644 --- a/docs/user-manuals/en/detectors/detector-catalog.md +++ b/docs/user-manuals/en/detectors/detector-catalog.md @@ -1,11 +1,11 @@ --- title: "Detector Catalog" -description: "All 64 built-in detectors grouped by category, with their IDs, what they detect, and their default severity." +description: "All 65 built-in detectors grouped by category, with their IDs, what they detect, and their default severity." --- # Detector Catalog -Leakwatch ships **64 built-in detectors** that cover a wide range of credential types — from cloud provider access keys and AI API tokens to database connection strings and private cryptographic keys. Each detector has a stable ID, a default severity, and (for most) a paired verifier that can confirm whether a found secret is still live. +Leakwatch ships **65 built-in detectors** that cover a wide range of credential types — from cloud provider access keys and AI API tokens to database connection strings and private cryptographic keys. Each detector has a stable ID and default severity. Verification capability is classified separately as direct-live, context-required, format-only, or unavailable; a registered verifier does not by itself prove live coverage. This page lists every built-in detector. For verification coverage details see [Verification Coverage](#/verification/verification-coverage). To add your own patterns, see [Custom Rules](#/detectors/custom-rules). @@ -57,7 +57,7 @@ This page lists every built-in detector. For verification coverage details see [ |----|---------|----------| | `github-token` | GitHub Personal Access Token — both the classic `ghp_` PAT and the fine-grained `github_pat_` PAT | Critical | | `github-oauth-token` | GitHub OAuth2 & installation token — `gho_`/`ghu_`/`ghr_`/`ghs_`, including new stateless (JWT-format) `ghs_` installation tokens | Critical | -| `gitlab-pat` | GitLab Personal Access Token — the classic `glpat-` token plus the newer routable prefixes: deploy (`gldt-`), runner (`glrt-`), CI/CD build & trigger (`glcbt-`/`glptt-`), OAuth application secret (`gloas-`), and feed (`glft-`) tokens; self-hosted GitLab hosts are detected for verification when present nearby | Critical | +| `gitlab-pat` | GitLab credential — the classic `glpat-` personal access token plus deploy (`gldt-`), runner (`glrt-`), CI/CD build and trigger (`glcbt-`/`glptt-`), OAuth application secret (`gloas-`), and feed (`glft-`) prefixes; repository URLs are never trusted for verification routing | Critical | | `bitbucket-app-password` | Bitbucket App Password | Critical | | `circleci-token` | CircleCI Personal API Token | High | | `npm-token` | NPM Access Token | High | @@ -91,7 +91,7 @@ This page lists every built-in detector. For verification coverage details see [ | `sendgrid-api-key` | SendGrid API Key | Critical | | `mailgun-api-key` | Mailgun API Key | Critical | | `postmark-server-token` | Postmark Server API Token | High | -| `twilio-api-key` | Twilio API Key | Critical | +| `twilio-api-key` | Twilio API Key Secret explicitly paired with a nearby `SK...` Key SID; a bare SID is not a secret and is not reported | Critical | ## Monitoring and Observability @@ -111,7 +111,7 @@ This page lists every built-in detector. For verification coverage details see [ | `redis-connection-string` | Redis Connection String | Critical | | `rabbitmq-connection-string` | RabbitMQ Connection String | Critical | | `snowflake-credentials` | Snowflake Connection Credentials | Critical | -| `supabase-service-key` | Supabase Service Role Key | Critical | +| `supabase-service-key` | Supabase Personal Access Token (`sbp_`; stable legacy detector ID) | Critical | ## Identity and Access @@ -132,13 +132,14 @@ This page lists every built-in detector. For verification coverage details see [ | ID | Detects | Severity | |----|---------|----------| | `generic-api-key` | Generic API Key | Medium | +| `structured-config-secret` | Contextual secret value in JSON/JSONC, high-confidence YAML and TOML scalar assignments, XML elements/attributes, or `.env` configuration | High | | `jwt` | JSON Web Token | High | | `private-key` | Private Key (RSA, SSH, DSA, EC, PGP), including PKCS8 `PRIVATE KEY` and password-protected `ENCRYPTED PRIVATE KEY` armor | Critical | | `ftp-credentials` | FTP/SFTP Credentials | Critical | --- -**Total: 64 built-in detectors.** +**Total: 65 built-in detectors.** ## Filtering by severity @@ -159,7 +160,7 @@ See [Severity and Filtering](#/configuration/severity-and-filtering) for the ful ## Verification coverage -Some detectors have a live verifier; others are format-validated only; ten have no verifier at all. See [Verification Coverage](#/verification/verification-coverage) for the complete breakdown. +Some detectors have a direct-live verifier, some need trusted or companion context, others are format-only, and eleven have no verifier. See [Verification Coverage](#/verification/verification-coverage) for the complete breakdown. ## See also diff --git a/docs/user-manuals/en/getting-started/how-it-works.md b/docs/user-manuals/en/getting-started/how-it-works.md index 7070c66..3ccd5f1 100644 --- a/docs/user-manuals/en/getting-started/how-it-works.md +++ b/docs/user-manuals/en/getting-started/how-it-works.md @@ -33,10 +33,10 @@ Every scan starts with a **Source** — an abstraction that emits chunks of data | Container image | `scan image` | Layer contents of an OCI/Docker image, daemonless | | AWS S3 | `scan s3` | Object contents from an S3 bucket | | Google Cloud Storage | `scan gcs` | Object contents from a GCS bucket | -| Slack | `scan slack` | Message text from channels and DMs | +| Slack | `scan slack` | Message text from channels/DMs and opt-in text-like attachments | :::note -Slack scanning covers **message text only**. The contents of files uploaded to Slack are not scanned. +Slack downloads no files by default. `--include-files` enables bounded scanning of text-like attachments and requires Slack's `files:read` scope. ::: Chunks flow into a buffered channel consumed by the worker pool. @@ -49,7 +49,7 @@ Scans respond to `SIGINT` / `SIGTERM`: when a cancellation signal arrives, the c ## 3. Aho-Corasick keyword pre-filter -Running 64 regex patterns on every chunk would be slow. Instead, the engine builds a single **Aho-Corasick multi-pattern automaton** at startup from the keyword lists declared by each detector. For each chunk, this automaton does a single linear pass and returns only the detectors whose keywords appeared in the chunk's bytes. +Running 65 detector patterns on every chunk would be slow. Instead, the engine builds a single **Aho-Corasick multi-pattern automaton** at startup from the keyword lists declared by each detector. For each chunk, this automaton does a single linear pass and returns only the detectors whose keywords appeared in the chunk's bytes. This means most detectors never run their regex on most chunks. Detectors that declare no keywords always run (they skip the pre-filter and proceed directly to regex). @@ -63,7 +63,7 @@ Each shortlisted detector runs its compiled **regular expression** against the c - A **redacted** representation safe for output. - Optional extra metadata (e.g. account ID for an AWS key). -Leakwatch ships **64 built-in detectors** across 60 packages, covering cloud providers, AI APIs, payment platforms, databases, messaging tools, version control, and more. You can add your own patterns via [custom YAML rules](#/detectors/custom-rules). +Leakwatch ships **65 built-in detectors** across 61 packages, covering cloud providers, AI APIs, payment platforms, databases, messaging tools, version control, structured configuration, and more. You can add your own patterns via [custom YAML rules](#/detectors/custom-rules). All detectors are registered at compile time using Go's `init()` function and blank imports (ADR-0004). There is no plugin loader or dynamic discovery at runtime. @@ -87,12 +87,12 @@ If the marker is present, the finding is silently dropped **before any network c After detection completes for all chunks, the engine passes findings to a separate **verification worker pool** (default 4 workers). Verification: -- Is guarded by a global **rate limiter** (default 10 requests per second) shared across all workers. -- Applies a **per-request timeout** (default 10 seconds) to every API call. +- Admits each actual API call through both a shared global and a per-detector **rate limiter** (the configured default is 10 requests per second for each ceiling). Format-only or missing-context paths consume no token; the shared global bucket bounds total traffic but does not guarantee fair provider scheduling. +- Applies a **per-finding verification-operation timeout** (default 10 seconds), including bounded regional fallback and request admission. - Makes only **read-only, non-destructive** calls to the provider (e.g. `sts:GetCallerIdentity` for AWS keys). - Marks each finding with one of four statuses: `verified_active`, `verified_inactive`, `unverified`, or `verify_error`. -Leakwatch ships **54 verifiers**, covering 84.4% of the 64 built-in detector types. The remaining 10 types (such as JWTs and generic API keys) cannot be safely verified and are always reported as `unverified`. +Leakwatch registers **54 verifier implementations** for 65 detector types (83.1%), but registry presence is not live coverage: 39 are direct-live, 9 require trusted issuer/region/companion context, and 6 are format-only. The remaining 11 types have no verifier. Context-required, format-only, and no-verifier findings remain `unverified` unless their stated live-verification preconditions are met. Pass `--no-verify` to skip this stage entirely — useful for fast, offline scans. @@ -103,13 +103,13 @@ For a deep dive into verification behavior and status meanings, see [How Verific Each finding receives a **deterministic ID** computed as: ``` -sha256(detectorID + redacted + filePath + line) → truncated to the first 16 bytes, - hex-encoded to a 32-character string +sha256(detectorID + redacted + filePath + decimal(line) + ":" + decimal(byteOffset)) + → truncated to the first 16 bytes and hex-encoded to a 32-character string ``` The result is a flat 32-character lowercase hex string (e.g. `447b5d2846d08ce25dd3d638cfe911ad`) — **not** a dashed UUID. The same secret at the same location always produces the same ID, making it safe to deduplicate findings across runs or track them in issue trackers. -**Shannon entropy** (range 0–8) is computed for each finding and exposed in output for informational purposes. At the engine level, the `detection.entropy.threshold` gate applies **only** to heuristic detectors that explicitly opt in — currently just `generic-api-key` — dropping a match whose entropy falls below the threshold to suppress low-randomness placeholders. Every structural (format-anchored) detector, such as `aws-access-key-id` or `github-token`, is never gated by entropy: a low-entropy match from those detectors still appears in results. Custom rules apply their own independent per-rule `entropy` threshold (see [Custom Rules](#/detectors/custom-rules)), separate from this engine-level gate. +When `detection.entropy.enabled` is true, **Shannon entropy** (range 0–8) is computed for each non-empty finding and exposed in output for informational purposes; when disabled, the field is absent rather than a synthetic zero. At the engine level, `detection.entropy.threshold` applies **only** to heuristic detectors that explicitly opt in — currently just `generic-api-key` — dropping a match whose entropy falls below the threshold to suppress low-randomness placeholders. Every structural (format-anchored) detector, such as `aws-access-key-id` or `github-token`, is never gated by entropy: a low-entropy match from those detectors still appears in results. Custom rules apply their own independent per-rule `entropy` threshold (see [Custom Rules](#/detectors/custom-rules)), separate from this engine-level gate. ## 8. Post-scan filters diff --git a/docs/user-manuals/en/getting-started/installation.md b/docs/user-manuals/en/getting-started/installation.md index 404801e..f5bd995 100644 --- a/docs/user-manuals/en/getting-started/installation.md +++ b/docs/user-manuals/en/getting-started/installation.md @@ -28,7 +28,7 @@ go install github.com/HodeTech/leakwatch@latest The binary is placed in `$(go env GOPATH)/bin`. Make sure that directory is on your `PATH`. :::note -`go install` always fetches the latest tagged release. To pin a specific version, replace `@latest` with a tag such as `@v1.5.0`. +`go install` always fetches the latest tagged release. To pin a specific version, replace `@latest` with the current release tag, `@v1.8.0`. ::: ## Docker @@ -47,8 +47,8 @@ Available tags: | Tag | Description | |-----|-------------| | `:latest` | Most recent release | -| `:v1.5.0` | Exact version pin | -| `:v1.5` | Minor-version pin (tracks patch releases) | +| `:v1.8.0` | Exact current-release pin | +| `:v1.7` | Current minor-version pin (tracks patch releases) | Mount the directory you want to scan to `/scan` inside the container. Flags and options work identically to the native binary — see [CLI Reference](#/reference/cli-reference) for the full list. @@ -86,7 +86,7 @@ leakwatch version Expected output: ```text -leakwatch v1.5.0 (commit: a3f9c12, built: 2026-05-10T08:22:00Z) +leakwatch v1.8.0 (commit: 1a2b3c4, built: 2026-08-11T12:00:00Z) ``` If the command is not found, check that the install directory is on your `PATH`. diff --git a/docs/user-manuals/en/getting-started/introduction.md b/docs/user-manuals/en/getting-started/introduction.md index ed764ae..56096ed 100644 --- a/docs/user-manuals/en/getting-started/introduction.md +++ b/docs/user-manuals/en/getting-started/introduction.md @@ -13,8 +13,8 @@ It is written in Go, ships as a single static binary with no runtime dependencie A leaked credential in a single commit — even one later deleted — can stay reachable in Git history forever and be exploited within minutes of being pushed. Leakwatch is designed to catch those secrets early and tell you which ones are *actually dangerous*: -- **Broad detection** — 64 built-in detectors covering cloud providers, AI APIs, payment platforms, databases, messaging tools, and more, plus your own YAML custom rules. -- **Verification, not just detection** — for 54 detector types Leakwatch can confirm whether a found secret is *still live* by making a controlled, read-only call to the provider. A verified-active key is an incident; an inactive one is noise. +- **Broad detection** — 65 built-in detectors covering cloud providers, AI APIs, payment platforms, databases, messaging tools, structured configuration, and more, plus your own YAML custom rules. +- **Truthful verification capability** — 39 detector types have a direct live check, 9 need trusted issuer/region/companion context, and 6 perform offline format validation. Registry count is never presented as live coverage. - **Many sources** — scan a local filesystem, a full Git history, an OCI/Docker image, AWS S3, Google Cloud Storage, and Slack messages. - **CI-native output** — JSON, SARIF (for GitHub Code Scanning), CSV, a colorized terminal table, and inline GitHub Actions annotations. - **Secret-safe by design** — discovered secrets are redacted by default and are never logged, cached, or written to disk. @@ -49,8 +49,8 @@ You don't have to understand the pipeline to use Leakwatch — but it explains w To set expectations accurately: - It does **not** rewrite Git history or remove secrets for you — it finds and reports them, and (with `--remediation`) tells you how to rotate them. -- Slack scanning covers **message text only**; scanning the *contents* of uploaded files is not implemented. -- Verification is available for many but not all secret types — 10 detector types (such as JWTs and generic API keys) cannot be safely verified and are always reported as unverified. +- Slack scans message text by default; bounded text-like attachments are opt-in with `--include-files` and the `files:read` scope. +- Verification is available for many but not all secret types — 11 detector types (such as JWTs and generic API keys) cannot be safely verified and are always reported as unverified. ## Next steps diff --git a/docs/user-manuals/en/getting-started/quick-start.md b/docs/user-manuals/en/getting-started/quick-start.md index 6538e37..c0f3a5b 100644 --- a/docs/user-manuals/en/getting-started/quick-start.md +++ b/docs/user-manuals/en/getting-started/quick-start.md @@ -62,7 +62,7 @@ Each row in the table (or object in JSON) represents one finding. The key fields | **STATUS** | Verification outcome: `verified_active`, `verified_inactive`, `unverified`, or `verify_error` | | **REMEDIATION** | Rotation/revocation guidance title, or `-` when `--remediation` was not passed | -A `verified_active` status means Leakwatch confirmed the secret is still live by making a read-only API call to the provider. **Treat every `verified_active` finding as an open incident.** +A `verified_active` status means Leakwatch confirmed the secret is still live with a contract-valid, non-destructive provider check. **Treat every `verified_active` finding as an open incident.** ## Common scan options diff --git a/docs/user-manuals/en/output/output-formats.md b/docs/user-manuals/en/output/output-formats.md index d68c519..f37ec59 100644 --- a/docs/user-manuals/en/output/output-formats.md +++ b/docs/user-manuals/en/output/output-formats.md @@ -52,6 +52,10 @@ leakwatch scan fs ./src --format json --output findings.json `id` is a deterministic, truncated SHA-256 rendered as a flat 32-character lowercase hex string — it is **not** a UUID and never contains dashes. See [How It Works](#/getting-started/how-it-works) for how it is computed. +`entropy` is omitted when it was not calculated; a calculated value of `0.0` is emitted as zero. `detected_at` is omitted for library-created findings that have no timestamp instead of emitting Go's year-1 zero time. Normal CLI findings set the timestamp. + +Provider-validated identity, scope, and expiry metadata appears under `verification.extra_data` when available. Values are bounded and schema-validated; absence means the provider did not supply safe metadata, not that the token has no permissions or expiry. + When `--remediation` is also set, a `"remediation"` object is nested inside each finding. See [Remediation Guidance](#/output/remediation). ## SARIF @@ -110,7 +114,7 @@ e6fa909746d7d5242309b64c33209fa9,aws-access-key-id,high,AKIA****K7NP,,config/aws ## Table -The `table` format writes a human-readable tab-aligned table, best suited for interactive terminal sessions where you want a quick visual scan of the results. +The `table` format writes a human-readable terminal-cell-aligned table, including correct display width for CJK, combining marks, and emoji. It is best suited for interactive terminal sessions where you want a quick visual scan of the results. **Columns:** @@ -118,9 +122,9 @@ The `table` format writes a human-readable tab-aligned table, best suited for in SEVERITY | DETECTOR | FILE | LINE | REDACTED | STATUS | REMEDIATION ``` -The `LINE` column shows `-` when no line number is available (for example a Slack or container-image finding). The `REMEDIATION` column is always present and shows `-` unless `--remediation` is set. When `--show-raw` is also set, a trailing `RAW` column is appended. A summary line is printed at the bottom of the table (e.g. `Found 3 secrets (1 critical, 2 high).`). +The `LINE` column shows `-` when no line number is available (for example a Slack or container-image finding). The `REMEDIATION` column is always present and shows `-` unless `--remediation` is set. When `--show-raw` is also set, a trailing `RAW` column is appended. In table output only, this value is a reversible Go-quoted string: quotes and escapes such as `\\n`, `\\t`, and `\\x1b` keep terminal control bytes inert while `strconv.Unquote` can recover the exact original bytes. JSON and CSV raw values retain their existing machine-readable representation. A summary line is printed at the bottom of the table (e.g. `Found 3 secrets (1 critical, 2 high).`). -Attacker-influenced fields (detector ID, file path, redacted value) are stripped of control characters and ANSI escape sequences before being written to the terminal, so a maliciously crafted file name or custom-rule match cannot inject escape sequences into your terminal session. +Attacker-influenced fields (detector ID, file path, redacted value) are stripped of control characters and ANSI escape sequences before being written to the terminal. Unicode bidi controls are rendered as visible `\\uXXXX` code points, preventing visual reordering without hiding their presence. Ordinary internationalized text, including CJK, combining marks, and emoji ZWJ sequences, is preserved. **ANSI color** is applied to the `SEVERITY` column automatically, but only when all four conditions are met: diff --git a/docs/user-manuals/en/reference/cli-reference.md b/docs/user-manuals/en/reference/cli-reference.md index db964b8..0d3233f 100644 --- a/docs/user-manuals/en/reference/cli-reference.md +++ b/docs/user-manuals/en/reference/cli-reference.md @@ -25,7 +25,7 @@ leakwatch version ``` ```text -leakwatch v1.5.0 (commit: a3f9c12, built: 2026-05-10T08:22:00Z) +leakwatch v1.8.0 (commit: 1a2b3c4, built: 2026-08-11T12:00:00Z) ``` ## `leakwatch init` @@ -67,6 +67,8 @@ The following flags are available on **all** `scan` subcommands. | `--exclude-detectors` | — | — | Detector IDs to exclude for this run (e.g. `aws-access-key-id,generic-api-key`). Repeatable or comma-separated; combined with (not a replacement for) `filter.exclude-detectors` in the config file. | | `--no-verify` | — | `false` | Disable live secret verification. No outbound API calls are made. | | `--only-verified` | — | `false` | Report only findings that Leakwatch has confirmed are active via live verification. | +| `--verifier-origin ` | — | — | Bind a context-required detector to an operator-trusted HTTPS origin. Repeatable; accepted only on the command line. | +| `--grafana-instance-url ` | — | — | Backward-compatible alias that supplies the trusted Grafana origin. | | `--min-severity` | — | `low` | Minimum severity to include in output: `low`, `medium`, `high`, or `critical`. An unrecognized value (e.g. a typo) is a hard error rather than being silently accepted. | | `--remediation` | — | `false` | Attach remediation guidance (rotation/revocation steps) to each finding. | @@ -252,7 +254,8 @@ No positional arguments. | `--exclude-channels ` | — | Comma-separated list of channel names or IDs to skip. | | `--since ` | — | Scan only messages posted after this date. | | `--include-dms` | `false` | Include direct messages (requires additional OAuth scopes). | -| `--rate-limit ` | `1` | Maximum Slack API requests per second. | +| `--include-files` | `false` | Download and scan bounded text-like attachments. Requires `files:read`. | +| `--rate-limit ` | `0` | Optional per-operation request cap per second. Zero preserves Slack-safe operation-specific defaults. | `scan slack` has no `--exclude` path-pattern flag (unlike every other scan subcommand) — use `--exclude-channels` to skip whole channels instead. diff --git a/docs/user-manuals/en/reference/exit-codes.md b/docs/user-manuals/en/reference/exit-codes.md index 329b560..0a8d4a3 100644 --- a/docs/user-manuals/en/reference/exit-codes.md +++ b/docs/user-manuals/en/reference/exit-codes.md @@ -26,7 +26,7 @@ Exit code `1` is only emitted when at least one finding survives all active outp This means exit code `0` means "no findings matched your current filter settings" — not necessarily that the codebase contains no secrets at all. :::warn -A clean `0` exit under `--only-verified` does not guarantee the codebase is secret-free. Secrets for which verification is unavailable (10 detector types) are always reported as unverified and are suppressed by `--only-verified`. Pair `--only-verified` with a separate unfiltered scan if you need full coverage. +A clean `0` exit under `--only-verified` does not guarantee the codebase is secret-free. Secrets for which verification is unavailable (11 detector types) are always reported as unverified and are suppressed by `--only-verified`. Pair `--only-verified` with a separate unfiltered scan if you need full coverage. ::: ## Using exit codes in shell scripts diff --git a/docs/user-manuals/en/scanning/slack.md b/docs/user-manuals/en/scanning/slack.md index b0fec82..c2da49e 100644 --- a/docs/user-manuals/en/scanning/slack.md +++ b/docs/user-manuals/en/scanning/slack.md @@ -1,14 +1,14 @@ --- title: "Slack Workspace" -description: "Scan Slack channel and DM message text for leaked secrets." +description: "Scan Slack messages and opt-in text attachments for leaked secrets." --- # Slack Workspace -Developers frequently share credentials in chat — a token pasted into a channel for a quick test, a password sent in a DM, or an API key mentioned in an incident thread. `leakwatch scan slack` reads message text across your Slack workspace and flags any secrets it finds. +Developers frequently share credentials in chat — a token pasted into a channel for a quick test, a password sent in a DM, or a configuration file uploaded to an incident thread. `leakwatch scan slack` reads message text across your Slack workspace and, when explicitly enabled, text-like file attachments. -:::warn -Leakwatch scans **message text only**. Scanning the contents of uploaded files (attachments, snippets) is not implemented. Only the text body of messages is analysed. +:::note +File attachment scanning is opt-in. Use `--include-files` and grant `files:read`; without the flag, Leakwatch downloads no attachments. ::: ## Basic usage @@ -34,10 +34,15 @@ The bot token must be associated with a Slack app that has the following OAuth s | Scope | Purpose | |-------|---------| +| `channels:read` | List public channels. | | `channels:history` | Read messages in public channels the bot has joined. | +| `groups:read` | List private channels. | | `groups:history` | Read messages in private channels the bot has joined. | +| `im:read` | List direct-message conversations (required only with `--include-dms`). | | `im:history` | Read direct messages (required only with `--include-dms`). | +| `mpim:read` | List group direct-message conversations (required only with `--include-dms`). | | `mpim:history` | Read group direct messages (required only with `--include-dms`). | +| `files:read` | Read file metadata and contents (required only with `--include-files`). | ## Flags @@ -50,7 +55,8 @@ The bot token must be associated with a Slack app that has the following OAuth s | `--exclude-channels` | string | — | Comma-separated list of channel names to skip. | | `--since` | string (YYYY-MM-DD) | — | Scan messages posted on or after this date. | | `--include-dms` | bool | `false` | Also scan direct messages and group DMs. | -| `--rate-limit` | float | `1` | Maximum Slack API requests per second. | +| `--include-files` | bool | `false` | Download and scan bounded text-like file attachments. Requires `files:read`. | +| `--rate-limit` | float | `0` | Optional per-operation Slack request cap per second. Zero retains safe operation-specific defaults: history uses `1/60`, while list/file/download defaults are higher. | ### Common scan flags @@ -59,7 +65,7 @@ The bot token must be associated with a Slack app that has the following OAuth s | `--format` | `-f` | `json` | Output format: `json`, `sarif`, `csv`, `table`, `github`. | | `--output` | `-o` | stdout | Write results to this file instead of stdout. | | `--concurrency` | `-c` | CPU count | Number of concurrent workers. | -| `--max-file-size` | — | `10485760` (10 MB) | Internal chunk size limit (bytes). | +| `--max-file-size` | — | `10485760` (10 MB) | Maximum attachment size buffered for scanning, in bytes. | | `--show-raw` | — | `false` | Include the raw secret value in output. | | `--exclude-detectors` | — | — | Detector IDs to exclude for this run. Repeatable; combined with `filter.exclude-detectors`. | | `--no-verify` | — | `false` | Disable secret verification. | @@ -94,10 +100,18 @@ leakwatch scan slack \ --include-dms ``` -Reduce the API request rate to avoid Slack rate-limit errors on large workspaces: +Include text-like file attachments while lowering the per-file memory bound: ```bash -leakwatch scan slack --rate-limit 10 --format table +leakwatch scan slack \ + --include-files \ + --max-file-size 5242880 +``` + +Raise the API request rate only for a Marketplace/internal app whose published tier permits it: + +```bash +leakwatch scan slack --rate-limit 0.8 --format table ``` Save only verified active findings to a JSON file: @@ -120,13 +134,16 @@ Each finding from a Slack scan includes message and channel metadata: | `message_user` | Slack user ID of the message author. There is no `author` field for Slack findings. | | `message_ts` | Slack message timestamp (unique message ID). | | `thread_ts` | Timestamp of the parent message, present only when the finding is in a threaded reply. | +| `file_path` | Synthetic `slack//` path, present for findings from an attachment. | ## Performance considerations -Slack API requests are subject to rate limits enforced by Slack. The `--rate-limit` flag (default `1` request/second) controls how aggressively Leakwatch makes requests. Raise it cautiously for a faster scan on a workspace with generous rate limits, or lower it further if you see `429 Too Many Requests` errors. +Slack API limits apply per method, workspace, and app. Leakwatch therefore uses independent limiter buckets: `conversations.history` defaults to one request/minute for newly distributed non-Marketplace apps, `conversations.list` follows Tier 2 (20+/minute), `files.info` follows Tier 4 (100+/minute), and attachment downloads have a separate conservative 100/minute client-side cap. `--rate-limit` deliberately overrides every bucket with the same per-operation ceiling; Marketplace/internal apps with Tier 3 history access can raise it deliberately. When Slack responds with `429 Too Many Requests`, Leakwatch automatically honors the `Retry-After` header and retries the request rather than failing the scan outright. +Attachment metadata and downloads use separate limiter buckets rather than consuming each other's or history capacity. Leakwatch accepts only Slack-owned HTTPS download URLs, transfers attachment chunks with direct backpressure, buffers at most `--max-file-size`, rejects invalid UTF-8/binary content even when MIME metadata is empty or spoofed, and de-duplicates the same Slack file ID across messages. + Use `--channels` to target specific channels rather than scanning the entire workspace on every run. Combine with `--since` to scan only recent messages incrementally. ## Exit codes diff --git a/docs/user-manuals/en/verification/how-verification-works.md b/docs/user-manuals/en/verification/how-verification-works.md index b50d7dc..d5aa6fa 100644 --- a/docs/user-manuals/en/verification/how-verification-works.md +++ b/docs/user-manuals/en/verification/how-verification-works.md @@ -14,15 +14,19 @@ After the scan engine collects findings, the verifier pool picks them up. Each f - If a verifier exists, it runs and returns a status. - If no verifier is registered for that detector type, the finding passes through unchanged with status `unverified`. -## Two verification modes +## Three verification modes -Not all secrets can be verified the same way. Leakwatch uses two distinct approaches depending on what is safe for each credential type. +Not all secrets can be verified the same way. Leakwatch distinguishes direct live checks, checks that require trusted or companion context, and offline format validation. Registry presence is therefore not treated as proof of live capability. ### Live API verification -For 48 detector types, Leakwatch makes a **controlled, read-only API call** to the provider — for example, calling `sts:GetCallerIdentity` for AWS keys or `GET /user` for GitHub tokens. The call uses only the minimum endpoint required to confirm identity; it never modifies data, creates resources, or triggers billing events. +For 39 detector types, Leakwatch can make a **controlled, non-destructive provider check** in the normal production path — for example, calling `sts:GetCallerIdentity` for AWS keys or a fixed provider identity endpoint for OpenAI keys. The call uses only the minimum endpoint required to confirm identity; it never modifies data or creates resources, though it may consume provider quota. -If the provider returns a success response, the finding is marked `verified_active`. If the provider rejects the credential (for example with HTTP 401, or HTTP 403 for a provider whose scoped-key errors are folded into "inactive"), the finding is marked `verified_inactive`. A few verifiers (for example SendGrid) distinguish a 403 caused by a narrowly scoped-but-valid key from a genuine rejection and still report `verified_active` in that case — see [Verification Coverage](#/verification/verification-coverage) for provider-specific notes. +If the provider returns a contract-valid success response, the finding is marked `verified_active`. A finding is marked `verified_inactive` only when the provider response is definitive under that verifier's contract. Permission denial and ambiguous responses remain `verify_error`; for example, SendGrid treats only HTTP `401` as inactive, while `403` remains inconclusive. + +### Trusted or companion context required + +Nine registered implementations cannot safely make a live request from a bare detector finding. Auth0, GitLab, Grafana, GitHub/GHES, Datadog, and Snyk require a trusted issuer/site/API origin. Twilio findings already contain the explicitly paired API Key Secret and non-secret Key SID, but still require a trusted regional origin. Shopify requires an operator-trusted issuing-store origin; finding metadata cannot select it. Supply these targets only with the repeatable `--verifier-origin detector-id=https://host` command-line flag (`--grafana-instance-url` is also retained for Grafana). Project configuration, environment variables, repository URLs, and token claims cannot set a verification target. Without explicit context Leakwatch sends no request and returns `unverified`. ### Format validation only @@ -49,7 +53,7 @@ Every finding in Leakwatch output carries one of four statuses: |--------|---------|-------------------| | `verified_active` | The secret was confirmed live by the provider. | Treat as an active incident. Rotate immediately. | | `verified_inactive` | The provider rejected the credential. | Likely already rotated. Review context and close. | -| `unverified` | No verifier exists for this type, or format validation returned no result, or verification was disabled. | Triage manually; context determines risk. | +| `unverified` | No verifier exists, required context is absent, a format-only verifier cannot prove liveness, or verification was disabled. | Triage manually; context determines risk. | | `verify_error` | The verifier ran but encountered a network error, timeout, or unexpected response. | Treat as potentially active. Retry or triage manually. | ## The verification engine @@ -59,8 +63,8 @@ Verification runs in a dedicated concurrent worker pool, isolated from the scan | Setting | Default | Config key | |---------|---------|-----------| | Worker count | 4 | `verification.concurrency` | -| Global rate limit | 10 requests/second | `verification.rate-limit` | -| Per-request timeout | 10 s | `verification.timeout` | +| Global + per-detector request ceilings | 10 requests/second each | `verification.rate-limit` | +| Per-finding verification-operation timeout | 10 s | `verification.timeout` | All three values are tunable under the `verification:` block in `.leakwatch.yaml`: @@ -68,14 +72,16 @@ All three values are tunable under the `verification:` block in `.leakwatch.yaml verification: enabled: true concurrency: 4 - rate-limit: 10.0 # requests per second (global) + rate-limit: 10.0 # requests per second (global and per detector) timeout: 10s ``` :::tip -If you are scanning a repository that triggers hundreds of findings, consider lowering `rate-limit` to 5 or enabling `--only-verified` to keep the verified-active set small and actionable. +For repositories with hundreds of findings, keep the provider-safe rate ceiling and reduce `concurrency` if you need gentler traffic. `--only-verified` changes only output filtering; it does not reduce verification requests. ::: +Admission occurs immediately before an actual provider request. Format-only and missing-context results do not consume limiter capacity. The per-detector bucket constrains that detector's own request rate, while the shared global bucket constrains aggregate traffic; this does not guarantee fair scheduling between providers. + ## Controlling verification at the command line **Disable verification entirely** with `--no-verify` (or set `verification.enabled: false` in config). Every finding passes through as `unverified`. Use this for offline or air-gapped environments, or when you want the fastest possible scan without touching any provider API. diff --git a/docs/user-manuals/en/verification/verification-coverage.md b/docs/user-manuals/en/verification/verification-coverage.md index 5bdf794..529ff18 100644 --- a/docs/user-manuals/en/verification/verification-coverage.md +++ b/docs/user-manuals/en/verification/verification-coverage.md @@ -1,28 +1,25 @@ --- title: "Verification Coverage" -description: "Which of the 64 built-in detectors are live-verified, format-validated only, or not verifiable — and what that means for triage." +description: "Which of the 65 built-in detectors are live-verified, context-required, format-only, or not verifiable — and what that means for triage." --- # Verification Coverage -Leakwatch ships 64 built-in detectors and 54 verifiers, giving a coverage rate of **84.4%** (54 of 64 detector types have some form of verification — either live or format-only). This page maps every detector to its verification status so you know what to expect in your output. +Leakwatch ships **65 built-in detectors** and 54 registered verifier implementations. Registry presence is not the same as live capability: **39** detectors can make a live check in the normal production path, **9** require trusted operator or companion context, **6** perform offline format validation only, and **11** have no verifier. This page maps every detector to its actual verification contract. -## Live-verified (48 detector types) +## Live-verified (39 detector types) -For these types, Leakwatch makes a controlled, read-only API call to the provider and returns `verified_active` or `verified_inactive`. No data is created or modified; the call uses the minimum endpoint needed to confirm identity. +For these types, Leakwatch can make a controlled, non-destructive provider check in the normal production path. A contract-valid success can return `verified_active`; only a definitive authentication rejection on the correct issuer can return `verified_inactive`. Ambiguous responses remain `verify_error`. | Detector type | Provider | |--------------|---------| | `aws-access-key-id` | AWS STS (`GetCallerIdentity`) | -| `github-token` | GitHub REST API | -| `github-oauth-token` | GitHub REST API | -| `gitlab-pat` | GitLab REST API (targets a self-hosted GitLab host when one is captured alongside the token; falls back to gitlab.com) | | `slack-token` | Slack Web API | | `openai-api-key` | OpenAI API | | `anthropic-api-key` | Anthropic API | | `deepseek-api-key` | DeepSeek API | | `huggingface-token` | Hugging Face API | -| `sendgrid-api-key` | SendGrid Web API (a `403` from a narrowly scoped/restricted key is treated as `verified_active`, not inactive, since the key itself is valid — only `401` maps to `verified_inactive`) | +| `sendgrid-api-key` | SendGrid Web API (`401` is inactive; `403`, including permission denial, remains `verify_error`) | | `mailgun-api-key` | Mailgun API (auto-detects and calls the correct EU vs. US regional endpoint) | | `postmark-server-token` | Postmark API | | `stripe-api-key-live` | Stripe API | @@ -41,27 +38,39 @@ For these types, Leakwatch makes a controlled, read-only API call to the provide | `telegram-bot-token` | Telegram Bot API | | `sentry-token` | Sentry API | | `pagerduty-api-key` | PagerDuty API | -| `newrelic-api-key` | New Relic API | -| `grafana-api-key` | Grafana API | -| `datadog-api-key` | Datadog API | -| `snyk-api-key` | Snyk API | -| `twilio-api-key` | Twilio API (authenticates with the API Key SID paired to its API Key Secret; without the paired secret the result is `unverified`, never a false `verified_inactive`) | +| `newrelic-api-key` | New Relic NerdGraph (bounded official US/EU fallback; inactive only when both regions return `401`) | | `doppler-token` | Doppler API | | `launchdarkly-sdk-key` | LaunchDarkly API | | `sonarcloud-token` | SonarCloud API | -| `shopify-access-token` | Shopify Admin API | | `notion-token` | Notion API | | `linear-api-key` | Linear API | | `figma-pat` | Figma REST API | | `airtable-pat` | Airtable API | | `okta-api-token` | Okta API (targets the org domain captured alongside the token) | -| `auth0-management-token` | Auth0 Management API (targets the tenant decoded from the token's own JWT `iss` claim) | | `databricks-token` | Databricks REST API (calls the workspace host captured alongside the token) | | `bitbucket-app-password` | Bitbucket REST API | -| `supabase-service-key` | Supabase API | +| `supabase-service-key` | Supabase Management API (`sbp_` personal access token; `401` is inactive, `403` remains `verify_error`) | | `infura-api-key` | Infura API | | `teams-webhook` | Microsoft Teams | +## Requires trusted or companion context (9 detector types) + +These implementations are registered, but a bare detector finding is not enough to choose a safe issuer or authenticate the verification request. When the required context is absent, Leakwatch makes no unsafe guess and returns `unverified`. + +Supply an origin only through the repeatable command-line flag `--verifier-origin detector-id=https://host`. This routing input is intentionally ignored in project configuration files and environment variables, so scanned content cannot redirect credentials. The older `--grafana-instance-url` flag remains a Grafana-only alias. + +| Detector ID | Required context | Production behavior | +|-------------|------------------|---------------------| +| `auth0-management-token` | Operator-trusted Auth0 tenant or custom-domain origin | Configure with `--verifier-origin auth0-management-token=https://tenant`. The detector emits a complete three-segment Management JWT, but unverified JWT claims and repository URLs never select a request target. The verifier uses a read-only clients probe and only `401` is inactive. | +| `gitlab-pat` | Operator-trusted GitLab.com or self-managed API origin | Configure with `--verifier-origin gitlab-pat=https://gitlab.example`. Repository content and finding metadata never select the request target. Only `glpat-` personal access tokens have the safe `/api/v4/user` probe; all other recognized GitLab token subtypes remain `unverified`. After active identity proof, a best-effort `/personal_access_tokens/self` call adds bounded, validated, sorted standard or GitLab 19.2+ granular scopes and ISO expiry to `verification.extra_data`; unavailable or malformed metadata never erases the active proof. A `401` is inactive only when its strict JSON body is GitLab's standard invalid-token response; DPoP challenges remain `verify_error`. | +| `grafana-api-key` | Trusted Grafana instance origin supplied with `--grafana-instance-url` | Calls only the validated HTTPS instance. Repository content and finding metadata cannot choose the target; `401` is inactive only on that trusted issuer. | +| `twilio-api-key` | Paired API Key SID plus an operator-trusted regional API origin (US1/IE1/AU1) | Configure with `--verifier-origin twilio-api-key=https://api.twilio.com` (or the correct regional Twilio origin). A bare `SK...` SID is a public identifier and is not reported. The detector emits an opaque, explicitly assigned secret only when it pairs one-to-one with a nearby assigned SID. Only `401` is inactive and permission `403` remains `verify_error`. | +| `shopify-access-token` | Operator-trusted issuing store origin | Configure with `--verifier-origin shopify-access-token=https://store.myshopify.com`. Finding metadata is never trusted for routing. The verifier uses the pinned 2026-07 Admin GraphQL shop identity query; only `401` on the selected store is inactive. | +| `github-token` | Trusted GitHub.com or GitHub Enterprise Server API origin | Configure with `--verifier-origin github-token=https://api.github.com` or the GHES origin. GHES uses the same `ghp_` and `github_pat_` formats as GitHub.com; repository metadata cannot choose the issuer. Validated scope/count and expiry response headers are added to `verification.extra_data` when the issuer provides them. | +| `github-oauth-token` | Trusted GitHub.com or GitHub Enterprise Server API origin | Configure with `--verifier-origin github-oauth-token=https://api.github.com` or the GHES origin. `gho_`/`ghu_` use `/user` and receive the same safe scope/expiry enrichment; `ghs_` uses the installation repositories endpoint. `ghr_` refresh tokens remain `unverified` without a request because exchanging one rotates it. | +| `datadog-api-key` | Trusted Datadog site/API origin | Configure the exact official site with `--verifier-origin datadog-api-key=https://api.datadoghq.com` (or its regional/FED host). Arbitrary hosts are rejected; a wrong region's rejection is not used as revocation evidence. | +| `snyk-api-key` | Trusted Snyk regional, government, or private API origin | Configure with `--verifier-origin snyk-api-key=https://api.snyk.io` (or the correct regional/private origin). A wrong region's rejection is not proof of revocation. Only `401` proves inactivity; `403` remains `verify_error` because it can mean a valid token lacks API-plan or endpoint permission. | + ## Format-validated only (6 detector types) These verifiers run entirely offline. No network request is made. Because a valid format does not prove a credential is active, all six always return `unverified` regardless of whether the format check passes or fails. @@ -75,7 +84,7 @@ These verifiers run entirely offline. No network request is made. Because a vali | `azure-entra-secret` | Format check | Client credential flow would create sessions | | `coinbase-api-key` | Character-set and length check | Coinbase's legacy API authenticates with HMAC-SHA256 request signing that requires the paired secret, which the detector cannot reliably associate with the key; live verification is not attempted so a real key is never misreported as inactive | -## Not verifiable (10 detector types) +## Not verifiable (11 detector types) These detector types have no verifier at all. Findings from them are always `unverified`. This is **not** because they are unimportant — they are detected and reported in full — but because no public verification API exists, or because any verification attempt would have side effects. @@ -91,20 +100,22 @@ These detector types have no verifier at all. Findings from them are always `unv | `slack-webhook` | Confirming a webhook is active requires sending a message | | `hashicorp-vault-token` | Vault token validation requires knowing the Vault endpoint | | `discord-webhook-url` | Confirming a webhook is active requires posting a message to it | +| `structured-config-secret` | Contextual fallback can identify a secret role but not the credential provider or issuer | :::note -"Not verifiable" does not mean "not found". All 10 of these types are still detected and appear in your output. They require manual triage to determine whether the credential is live and whether it needs rotation. +"Not verifiable" does not mean "not found". All 11 of these types are still detected and appear in your output. They require manual triage to determine whether the credential is live and whether it needs rotation. ::: ## Coverage summary | Category | Count | |----------|-------| -| Live-verified | 48 | +| Live-verified | 39 | +| Requires trusted/companion context | 9 | | Format-validated only | 6 | -| Not verifiable | 10 | -| **Total detectors** | **64** | -| **Verifiers (any coverage)** | **54 (84.4%)** | +| Not verifiable | 11 | +| **Total detectors** | **65** | +| **Registered verifier implementations** | **54 (83.1%)** | ## See also diff --git a/docs/user-manuals/tr/ci-cd/docker-usage.md b/docs/user-manuals/tr/ci-cd/docker-usage.md index 4f52c8f..76d5ebd 100644 --- a/docs/user-manuals/tr/ci-cd/docker-usage.md +++ b/docs/user-manuals/tr/ci-cd/docker-usage.md @@ -16,8 +16,8 @@ ghcr.io/hodetech/leakwatch | Etiket | Açıklama | |--------|----------| | `:latest` | En son sürüm | -| `:v1.5.0` | Tam sürüm sabitleme | -| `:v1.5` | Küçük sürüm sabitleme (yama sürümlerini takip eder) | +| `:v1.8.0` | Güncel sürüme tam sabitleme | +| `:v1.7` | Güncel küçük sürüme sabitleme (yama sürümlerini takip eder) | İmaj Alpine tabanlıdır, root olmayan `leakwatch` kullanıcısı olarak çalışır, çalışma dizini olarak `/scan` kullanır ve giriş noktası olarak `leakwatch`'ı ayarlar. diff --git a/docs/user-manuals/tr/ci-cd/github-action.md b/docs/user-manuals/tr/ci-cd/github-action.md index fc86f88..bff351e 100644 --- a/docs/user-manuals/tr/ci-cd/github-action.md +++ b/docs/user-manuals/tr/ci-cd/github-action.md @@ -88,7 +88,7 @@ SARIF yüklemesi, işin `permissions: security-events: write` bildirmesini gerek | `working-directory` | `.` | Taramanın çalıştırılacağı dizin. | | `sarif-upload` | `false` | Taramadan sonra SARIF sonuçlarını GitHub Code Scanning'e yükle. | | `fail-on-findings` | `true` | Bulgular raporlandığında (çıkış kodu 1) iş akışı adımını başarısız kıl. `false` olarak ayarlandığında adım başarısız olmak yerine `::warning::` ek açıklaması yayar. Ciddi hatalar (çıkış kodu ≥ 2) bu ayardan bağımsız olarak her zaman adımı başarısız kılar. | -| `version` | `latest` | Kurulacak Leakwatch sürümü: `latest` veya belirli bir sürümü sabitlemek için `v1.5.0` gibi bir etiket. | +| `version` | `latest` | Kurulacak Leakwatch sürümü: `latest` veya belirli bir sürümü sabitlemek için `v1.8.0` gibi bir etiket. | | `release-repo` | `HodeTech/Leakwatch` | Sürüm ikilisinin indirileceği depo (`owner/name`). Yalnızca fork veya kendi sunucunuzdaki aynalar için değiştirin. | ## Çıktılar @@ -144,10 +144,10 @@ Yeniden üretilebilir derlemeler için `version` değerini belirli bir etikete s ```yaml - uses: HodeTech/Leakwatch@v1 with: - version: "v1.5.0" + version: "v1.8.0" ``` -Bu, önceden derlenmiş `v1.5.0` ikilisini [Leakwatch sürümlerinden](https://github.com/HodeTech/Leakwatch/releases) indirir ve çalıştırmadan önce SHA-256 sağlama toplamını doğrular. En yüksek tedarik zinciri güvenliği için action'ın kendisini de bir commit SHA'sına sabitleyebilirsiniz: `uses: HodeTech/Leakwatch@`. +Bu, önceden derlenmiş `v1.8.0` ikilisini [Leakwatch sürümlerinden](https://github.com/HodeTech/Leakwatch/releases) indirir ve çalıştırmadan önce SHA-256 sağlama toplamını doğrular. En yüksek tedarik zinciri güvenliği için action'ın kendisini de bir commit SHA'sına sabitleyebilirsiniz: `uses: HodeTech/Leakwatch@`. ## Yalnızca değişen kodu tarama (pull-request diff) diff --git a/docs/user-manuals/tr/ci-cd/other-ci.md b/docs/user-manuals/tr/ci-cd/other-ci.md index 65043c7..9f98975 100644 --- a/docs/user-manuals/tr/ci-cd/other-ci.md +++ b/docs/user-manuals/tr/ci-cd/other-ci.md @@ -20,7 +20,7 @@ go install github.com/HodeTech/leakwatch@latest Yeniden üretilebilir derlemeler için belirli bir sürüme sabitleyin: ```bash -go install github.com/HodeTech/leakwatch@v1.5.0 +go install github.com/HodeTech/leakwatch@v1.8.0 ``` ### Docker imajı aracılığıyla (Go gerekmez) @@ -76,7 +76,7 @@ leakwatch: stage: test image: golang:1.25-alpine script: - - go install github.com/HodeTech/leakwatch@v1.5.0 + - go install github.com/HodeTech/leakwatch@v1.8.0 - leakwatch scan fs . --format json -o leakwatch.json --no-verify artifacts: when: always @@ -108,7 +108,7 @@ leakwatch scan fs . --no-verify --format sarif -o results.sarif ```yaml - script: | - go install github.com/HodeTech/leakwatch@v1.5.0 + go install github.com/HodeTech/leakwatch@v1.8.0 leakwatch scan fs . --format sarif -o $(Build.ArtifactStagingDirectory)/leakwatch.sarif --no-verify displayName: "Leakwatch sır taraması" @@ -124,7 +124,7 @@ leakwatch scan fs . --no-verify --format sarif -o results.sarif stage('Sır taraması') { steps { sh ''' - go install github.com/HodeTech/leakwatch@v1.5.0 + go install github.com/HodeTech/leakwatch@v1.8.0 leakwatch scan fs . --format json -o leakwatch.json --no-verify ''' archiveArtifacts artifacts: 'leakwatch.json', allowEmptyArchive: true diff --git a/docs/user-manuals/tr/ci-cd/pre-commit.md b/docs/user-manuals/tr/ci-cd/pre-commit.md index de931fa..55327ce 100644 --- a/docs/user-manuals/tr/ci-cd/pre-commit.md +++ b/docs/user-manuals/tr/ci-cd/pre-commit.md @@ -22,7 +22,7 @@ Deponuzun köküne bir `.pre-commit-config.yaml` dosyası ekleyin (veya mevcut o ```yaml repos: - repo: https://github.com/HodeTech/Leakwatch - rev: v1.5.0 + rev: v1.8.0 hooks: - id: leakwatch ``` @@ -56,7 +56,7 @@ Kancanın varsayılan davranışı, ek bayrak olmadan `leakwatch scan fs`'e kar ```yaml repos: - repo: https://github.com/HodeTech/Leakwatch - rev: v1.5.0 + rev: v1.8.0 hooks: - id: leakwatch args: @@ -105,7 +105,7 @@ SKIP=leakwatch git commit -m "chore: test sabiti ekle" `rev:` değerini dal adı yerine belirli bir etikete sabitleyin. Bu, ekipteki tüm geliştiricilerin aynı dedektör setini kullandığını ve kancanın sprint ortasında sessizce yükseltilmediğini garantiler: ```yaml -rev: v1.5.0 # sabitle; 'main' veya 'HEAD' kullanmayın +rev: v1.8.0 # sabitle; 'main' veya 'HEAD' kullanmayın ``` Güncellemek için: diff --git a/docs/user-manuals/tr/configuration/config-file.md b/docs/user-manuals/tr/configuration/config-file.md index f660fba..54c8b32 100644 --- a/docs/user-manuals/tr/configuration/config-file.md +++ b/docs/user-manuals/tr/configuration/config-file.md @@ -99,7 +99,8 @@ verification: # Sağlayıcı API'lerine karşı canlı doğrulamayı etkinleştirir. enabled: true - # İstek başına HTTP zaman aşımı. Doğrulama etkinleştirildiğinde >= 1ms olmalıdır. + # Sınırlı fallback ve istek kabulü dahil bulgu başına doğrulama operasyonu + # zaman aşımı. Doğrulama etkinleştirildiğinde >= 1ms olmalıdır. # Süre dizesi kullanın (örn. "10s", "500ms") — tam sayı nanosaniye olarak # yorumlanır ve doğrulama başarısız olur. timeout: 10s @@ -176,7 +177,7 @@ custom-rules: [] ``` :::note -`detection.entropy.threshold`, bir bulgunun yanında gösterilen entropi değerini kontrol eder ve entropi tabanlı sezgisel yöntemleri tercih eden her dedektörü kapılar — şu anda yalnızca yerleşik `generic-api-key` dedektörü, artı kendi `entropy` alanını bildiren özel kurallar. Bunlardan birine ait, entropisi eşiğin altına düşen bir eşleşme, olası bir yer tutucu olarak bastırılır. Diğer her (yapısal/biçim-çapalı) yerleşik dedektör — `aws-access-key-id`, `github-token` ve diğerleri — kendi sabit eşleşme kriterlerine sahiptir ve entropiden bağımsız olarak bu ayar tarafından **hiçbir zaman** bırakılmaz. +`detection.entropy.enabled`, hesaplanan entropi değerinin bulguların yanında bulunup bulunmadığını kontrol eder. `detection.entropy.threshold` gösterimi kontrol etmez; motor entropi sezgisini tercih eden dedektörleri kapılar — şu anda yalnızca yerleşik `generic-api-key` dedektörü. Entropisi eşiğin altına düşen bir eşleşme olası bir yer tutucu olarak bastırılır. Kendi `entropy` alanını bildiren özel kurallar bağımsız kural-başı eşiklerini uygular. Diğer her yapısal/biçim-çapalı yerleşik dedektör — `aws-access-key-id`, `github-token` ve diğerleri — entropiden bağımsız olarak motor eşiği tarafından **hiçbir zaman** bırakılmaz. ::: ## Doğrulama diff --git a/docs/user-manuals/tr/detectors/custom-rules.md b/docs/user-manuals/tr/detectors/custom-rules.md index e17d2f3..3670e34 100644 --- a/docs/user-manuals/tr/detectors/custom-rules.md +++ b/docs/user-manuals/tr/detectors/custom-rules.md @@ -1,11 +1,11 @@ --- title: "Özel Kurallar" -description: "YAML ile kendi sır tespit kalıplarınızı nasıl tanımlayacağınız ve 64 yerleşik dedektörün yanında bir Leakwatch taramasına nasıl ekleyeceğiniz." +description: "YAML ile kendi sır tespit kalıplarınızı nasıl tanımlayacağınız ve 65 yerleşik dedektörün yanında bir Leakwatch taramasına nasıl ekleyeceğiniz." --- # Özel Kurallar -64 yerleşik dedektör yaygın kullanılan kimlik bilgisi formatlarını kapsar; ancak her kuruluşun dahili token'ları, özel servis anahtarları veya hiçbir genel aracın önceden tahmin edemeyeceği ortama özgü kalıpları vardır. Özel kurallar, kaynak kodu değiştirmeden veya ikili dosyayı yeniden derlemeden kendi kalıplarınızı düz YAML ile tanımlamanıza ve çalışma zamanında yüklemenize olanak tanıyarak Leakwatch'ı genişletmenizi sağlar. +65 yerleşik dedektör yaygın kullanılan kimlik bilgisi formatlarını kapsar; ancak her kuruluşun dahili token'ları, özel servis anahtarları veya hiçbir genel aracın önceden tahmin edemeyeceği ortama özgü kalıpları vardır. Özel kurallar, kaynak kodu değiştirmeden veya ikili dosyayı yeniden derlemeden kendi kalıplarınızı düz YAML ile tanımlamanıza ve çalışma zamanında yüklemenize olanak tanıyarak Leakwatch'ı genişletmenizi sağlar. ## Özel kurallar nerede tanımlanır @@ -116,5 +116,5 @@ filter: ## Ayrıca bakın - [Yapılandırma: Yapılandırma Dosyası](#/configuration/config-file) — `custom-rules:` öğesinin belge yapısındaki yeri dahil `.leakwatch.yaml` için tam referans. -- [Dedektör Kataloğu](#/detectors/detector-catalog) — özel kuralınızı adlandırmadan önce ID çakışmalarını kontrol etmek için 64 yerleşik dedektör. +- [Dedektör Kataloğu](#/detectors/detector-catalog) — özel kuralınızı adlandırmadan önce ID çakışmalarını kontrol etmek için 65 yerleşik dedektör. - [Nasıl Çalışır](#/getting-started/how-it-works) — `keywords` öğesinin bağlandığı Aho-Corasick ön-filtre hattı. diff --git a/docs/user-manuals/tr/detectors/detector-catalog.md b/docs/user-manuals/tr/detectors/detector-catalog.md index 9b577be..c95dd1a 100644 --- a/docs/user-manuals/tr/detectors/detector-catalog.md +++ b/docs/user-manuals/tr/detectors/detector-catalog.md @@ -1,11 +1,11 @@ --- title: "Dedektör Kataloğu" -description: "Kategorilere göre gruplanmış tüm 64 yerleşik dedektör; ID'leri, ne tespit ettikleri ve varsayılan önem dereceleri ile." +description: "Kategorilere göre gruplanmış tüm 65 yerleşik dedektör; ID'leri, ne tespit ettikleri ve varsayılan önem dereceleri ile." --- # Dedektör Kataloğu -Leakwatch, bulut sağlayıcısı erişim anahtarlarından ve yapay zekâ API token'larından veritabanı bağlantı dizelerine ve özel kriptografik anahtarlara kadar geniş bir kimlik bilgisi türü yelpazesini kapsayan **64 yerleşik dedektör** ile gelir. Her dedektörün kararlı bir ID'si, varsayılan bir önem derecesi ve (çoğu için) bulunan sırrın hâlâ canlı olup olmadığını teyit edebilen eşleştirilmiş bir doğrulayıcısı vardır. +Leakwatch, bulut sağlayıcısı erişim anahtarlarından ve yapay zekâ API token'larından veritabanı bağlantı dizelerine ve özel kriptografik anahtarlara kadar geniş bir kimlik bilgisi türü yelpazesini kapsayan **65 yerleşik dedektör** ile gelir. Her dedektörün kararlı bir ID'si ve varsayılan önem derecesi vardır. Doğrulama kabiliyeti ayrıca doğrudan-canlı, bağlam-gerektiren, yalnız-format veya kullanılamaz olarak sınıflandırılır; kayıtlı bir doğrulayıcı tek başına canlı kapsamı kanıtlamaz. Bu sayfa her yerleşik dedektörü listeler. Doğrulama kapsamı ayrıntıları için [Doğrulama Kapsamı](#/verification/verification-coverage) bölümüne bakın. Kendi kalıplarınızı eklemek için [Özel Kurallar](#/detectors/custom-rules) bölümüne bakın. @@ -57,7 +57,7 @@ Bu sayfa her yerleşik dedektörü listeler. Doğrulama kapsamı ayrıntıları |----|------------|--------| | `github-token` | GitHub Kişisel Erişim Token'ı — hem klasik `ghp_` PAT hem de ince taneli `github_pat_` PAT | Critical | | `github-oauth-token` | GitHub OAuth2 ve kurulum (installation) token'ı — `gho_`/`ghu_`/`ghr_`/`ghs_`, yeni durumsuz (JWT biçimli) `ghs_` kurulum token'ları dâhil | Critical | -| `gitlab-pat` | GitLab Kişisel Erişim Token'ı — klasik `glpat-` token'ı artı yeni yönlendirilebilir önekler: dağıtım (`gldt-`), runner (`glrt-`), CI/CD derleme ve tetikleyici (`glcbt-`/`glptt-`), OAuth uygulama sırrı (`gloas-`) ve besleme (`glft-`) token'ları; yakınında bulunduğunda kendi barındırılan (self-hosted) GitLab sunucuları doğrulama için tespit edilir | Critical | +| `gitlab-pat` | GitLab kimlik bilgisi — klasik `glpat-` kişisel erişim token'ı ile dağıtım (`gldt-`), runner (`glrt-`), CI/CD derleme ve tetikleyici (`glcbt-`/`glptt-`), OAuth uygulama sırrı (`gloas-`) ve besleme (`glft-`) önekleri; repo URL'leri doğrulama yönlendirmesinde asla güvenilir sayılmaz | Critical | | `bitbucket-app-password` | Bitbucket Uygulama Parolası | Critical | | `circleci-token` | CircleCI Kişisel API Token'ı | High | | `npm-token` | NPM Erişim Token'ı | High | @@ -91,7 +91,7 @@ Bu sayfa her yerleşik dedektörü listeler. Doğrulama kapsamı ayrıntıları | `sendgrid-api-key` | SendGrid API Anahtarı | Critical | | `mailgun-api-key` | Mailgun API Anahtarı | Critical | | `postmark-server-token` | Postmark Sunucu API Token'ı | High | -| `twilio-api-key` | Twilio API Anahtarı | Critical | +| `twilio-api-key` | Yakınındaki `SK...` Key SID ile açıkça eşleştirilmiş Twilio API Key Secret; tek başına SID gizli değildir ve raporlanmaz | Critical | ## İzleme ve Gözlemlenebilirlik @@ -111,7 +111,7 @@ Bu sayfa her yerleşik dedektörü listeler. Doğrulama kapsamı ayrıntıları | `redis-connection-string` | Redis Bağlantı Dizesi | Critical | | `rabbitmq-connection-string` | RabbitMQ Bağlantı Dizesi | Critical | | `snowflake-credentials` | Snowflake Bağlantı Kimlik Bilgileri | Critical | -| `supabase-service-key` | Supabase Servis Rolü Anahtarı | Critical | +| `supabase-service-key` | Supabase Kişisel Erişim Token'ı (`sbp_`; kararlı eski dedektör ID'si) | Critical | ## Kimlik ve Erişim @@ -132,13 +132,14 @@ Bu sayfa her yerleşik dedektörü listeler. Doğrulama kapsamı ayrıntıları | ID | Tespit eder | Önem Derecesi | |----|------------|--------| | `generic-api-key` | Genel API Anahtarı | Medium | +| `structured-config-secret` | JSON/JSONC, yüksek güvenli YAML ve TOML skaler atamaları, XML öğe/öznitelikleri veya `.env` yapılandırmasında bağlamsal sır değeri | High | | `jwt` | JSON Web Token | High | | `private-key` | Özel Anahtar (RSA, SSH, DSA, EC, PGP); PKCS8 `PRIVATE KEY` ve parola korumalı `ENCRYPTED PRIVATE KEY` zarfı dahil | Critical | | `ftp-credentials` | FTP/SFTP Kimlik Bilgileri | Critical | --- -**Toplam: 64 yerleşik dedektör.** +**Toplam: 65 yerleşik dedektör.** ## Önem derecesine göre filtreleme @@ -159,7 +160,7 @@ Tam filtreleme referansı için [Önem Derecesi & Filtreleme](#/configuration/se ## Doğrulama kapsamı -Bazı dedektörlerin canlı doğrulayıcısı vardır; bazıları yalnızca format doğrulamasına tabi tutulur; onu ise hiç doğrulayıcıya sahip değildir. Tam döküm için [Doğrulama Kapsamı](#/verification/verification-coverage) bölümüne bakın. +Bazı dedektörlerin doğrudan canlı doğrulayıcısı vardır, bazıları güvenilir veya eşlik eden bağlam gerektirir, bazıları yalnızca format doğrular ve on birinin hiç doğrulayıcısı yoktur. Tam döküm için [Doğrulama Kapsamı](#/verification/verification-coverage) bölümüne bakın. ## Ayrıca bakın diff --git a/docs/user-manuals/tr/getting-started/how-it-works.md b/docs/user-manuals/tr/getting-started/how-it-works.md index c593d9b..3107cfe 100644 --- a/docs/user-manuals/tr/getting-started/how-it-works.md +++ b/docs/user-manuals/tr/getting-started/how-it-works.md @@ -33,10 +33,10 @@ Her tarama, motorun işlemesi için veri parçaları yayan bir soyutlama olan ** | Konteyner imajı | `scan image` | Bir OCI/Docker imajının katman içerikleri, daemonsuz | | AWS S3 | `scan s3` | Bir S3 kovasındaki nesne içerikleri | | Google Cloud Storage | `scan gcs` | Bir GCS kovasındaki nesne içerikleri | -| Slack | `scan slack` | Kanal ve DM'lerdeki mesaj metni | +| Slack | `scan slack` | Kanal/DM mesajları ve isteğe bağlı metin benzeri ekler | :::note -Slack taraması yalnızca **mesaj metnini** kapsar. Slack'e yüklenen dosyaların içerikleri taranmaz. +Slack varsayılan olarak hiçbir dosya indirmez. `--include-files`, metin benzeri eklerin boyutu sınırlı taramasını etkinleştirir ve Slack `files:read` kapsamını gerektirir. ::: Parçalar, işçi havuzu tarafından tüketilen tamponlu bir kanala akar. @@ -49,7 +49,7 @@ Taramalar `SIGINT` / `SIGTERM`'e yanıt verir: iptal sinyali geldiğinde bağlam ## 3. Aho-Corasick anahtar kelime ön-filtresi -Her parça üzerinde 64 regex desenini çalıştırmak yavaş olur. Bunun yerine motor, başlangıçta her dedektörün bildirdiği anahtar kelime listelerinden tek bir **Aho-Corasick çok-desenli otomat** oluşturur. Her parça için bu otomat tek bir doğrusal geçiş yapar ve yalnızca anahtar kelimeleri parçanın baytlarında görünen dedektörleri döndürür. +Her parça üzerinde 65 dedektör desenini çalıştırmak yavaş olur. Bunun yerine motor, başlangıçta her dedektörün bildirdiği anahtar kelime listelerinden tek bir **Aho-Corasick çok-desenli otomat** oluşturur. Her parça için bu otomat tek bir doğrusal geçiş yapar ve yalnızca anahtar kelimeleri parçanın baytlarında görünen dedektörleri döndürür. Bu, çoğu dedektörün çoğu parça üzerinde regex'ini hiç çalıştırmadığı anlamına gelir. Anahtar kelime bildirmeyen dedektörler her zaman çalışır (ön filtreyi atlayarak doğrudan regex'e geçerler). @@ -63,7 +63,7 @@ Kısa listeye alınan her dedektör, derlenmiş **düzenli ifadesini** parça ba - Çıktı için güvenli olan **maskelenmiş** bir gösterim. - İsteğe bağlı ek meta veri (örneğin bir AWS anahtarı için hesap kimliği). -Leakwatch, 60 paket genelinde **64 yerleşik dedektör** ile birlikte gelir; bulut sağlayıcılarını, yapay zekâ API'lerini, ödeme platformlarını, veritabanlarını, mesajlaşma araçlarını, sürüm kontrolünü ve daha fazlasını kapsar. [Özel YAML kuralları](#/detectors/custom-rules) aracılığıyla kendi desenlerinizi ekleyebilirsiniz. +Leakwatch, 61 paket genelinde **65 yerleşik dedektör** ile birlikte gelir; bulut sağlayıcılarını, yapay zekâ API'lerini, ödeme platformlarını, veritabanlarını, mesajlaşma araçlarını, sürüm kontrolünü, yapılandırılmış config dosyalarını ve daha fazlasını kapsar. [Özel YAML kuralları](#/detectors/custom-rules) aracılığıyla kendi desenlerinizi ekleyebilirsiniz. Tüm dedektörler, Go'nun `init()` işlevi ve boş importlar kullanılarak derleme zamanında kaydedilir (ADR-0004). Çalışma zamanında eklenti yükleyici veya dinamik keşif yoktur. @@ -87,12 +87,12 @@ veya dedektöre özgü bir varyant: Tüm parçalar için tespit tamamlandıktan sonra motor, bulguları ayrı bir **doğrulama işçi havuzuna** geçirir (varsayılan 4 işçi). Doğrulama: -- Tüm işçiler arasında paylaşılan global bir **hız sınırlayıcı** (varsayılan saniyede 10 istek) ile korunur. -- Her API çağrısına **istek başına zaman aşımı** (varsayılan 10 saniye) uygular. +- Her gerçek API çağrısını paylaşılan global ve dedektör başına birer **hız sınırlayıcıdan** geçirir (yapılandırılmış varsayılan değer her iki tavan için saniyede 10 istektir). Yalnız-format veya eksik-bağlam yolları token tüketmez; paylaşılan global kova toplam trafiği sınırlar ancak sağlayıcılar arasında adil zamanlama garantisi vermez. +- Sınırlı bölgesel fallback ve istek kabulü dahil olmak üzere her bulguya ait doğrulama operasyonuna **zaman aşımı** (varsayılan 10 saniye) uygular. - Sağlayıcıya yalnızca **salt-okunur, yıkıcı olmayan** çağrılar yapar (örneğin AWS anahtarları için `sts:GetCallerIdentity`). - Her bulguyu dört durumdan biriyle işaretler: `verified_active`, `verified_inactive`, `unverified` veya `verify_error`. -Leakwatch **54 doğrulayıcı** ile birlikte gelir; 64 yerleşik dedektör türünün %84,4'ünü kapsar. Kalan 10 tür (JWT'ler ve genel API anahtarları gibi) güvenli biçimde doğrulanamaz ve her zaman `unverified` olarak raporlanır. +Leakwatch 65 dedektör türü için **54 doğrulayıcı implementasyonu** kaydeder (%83,1); ancak registry kaydı canlı kapsam değildir: 39'u doğrudan canlı, 9'u güvenilir issuer/bölge/eşlik eden bağlam gerektiren, 6'sı yalnız-format doğrulayıcısıdır. Kalan 11 türün doğrulayıcısı yoktur. Bağlam-gerektiren, yalnız-format ve doğrulayıcısız bulgular, belirtilen canlı doğrulama önkoşulları karşılanmadıkça `unverified` kalır. Bu aşamayı tamamen atlamak için `--no-verify` geçirin — hızlı, çevrimdışı taramalar için kullanışlıdır. @@ -103,13 +103,13 @@ Doğrulama davranışı ve durum anlamları hakkında derinlemesine bilgi için Her bulgu, şu şekilde hesaplanan **deterministik bir kimlik** alır: ``` -sha256(detectorID + redacted + filePath + line) → ilk 16 bayta kısaltılır, - 32 karakterlik bir dizeye hex kodlanır +sha256(detectorID + redacted + filePath + decimal(line) + ":" + decimal(byteOffset)) + → ilk 16 bayta kısaltılır ve 32 karakterlik bir dizeye hex kodlanır ``` Sonuç, düz, 32 karakterlik küçük harfli bir hex dizesidir (örn. `447b5d2846d08ce25dd3d638cfe911ad`) — **çizgili** bir UUID **değildir**. Aynı konumdaki aynı sır her zaman aynı kimliği üretir; bu da bulguları çalıştırmalar arasında yinelenenleri kaldırmayı veya sorun izleyicilerde takip etmeyi güvenli kılar. -**Shannon entropisi** (aralık 0–8) her bulgu için hesaplanır ve bilgilendirme amacıyla çıktıda gösterilir. Motor düzeyinde, `detection.entropy.threshold` kapısı yalnızca açıkça buna dahil olan sezgisel dedektörlere uygulanır — şu anda yalnızca `generic-api-key` — entropisi eşiğin altına düşen bir eşleşmeyi, düşük rastgelelikli yer tutucuları bastırmak için düşürür. `aws-access-key-id` veya `github-token` gibi her yapısal (biçim-çapalı) dedektör entropi tarafından hiçbir zaman kapılanmaz: bu dedektörlerden gelen düşük entropili bir eşleşme yine de sonuçlarda görünür. Özel kurallar, bu motor düzeyindeki kapıdan ayrı olarak kendi bağımsız kural başına `entropy` eşiğini uygular (bkz. [Özel Kurallar](#/detectors/custom-rules)). +`detection.entropy.enabled` doğru olduğunda **Shannon entropisi** (aralık 0–8) boş olmayan her bulgu için hesaplanır ve bilgilendirme amacıyla çıktıda gösterilir; devre dışıyken yapay sıfır yerine alan bulunmaz. Motor düzeyinde, `detection.entropy.threshold` kapısı yalnızca açıkça buna dahil olan sezgisel dedektörlere uygulanır — şu anda yalnızca `generic-api-key` — entropisi eşiğin altına düşen bir eşleşmeyi, düşük rastgelelikli yer tutucuları bastırmak için düşürür. `aws-access-key-id` veya `github-token` gibi her yapısal (biçim-çapalı) dedektör entropi tarafından hiçbir zaman kapılanmaz: bu dedektörlerden gelen düşük entropili bir eşleşme yine de sonuçlarda görünür. Özel kurallar, bu motor düzeyindeki kapıdan ayrı olarak kendi bağımsız kural başına `entropy` eşiğini uygular (bkz. [Özel Kurallar](#/detectors/custom-rules)). ## 8. Tarama sonrası filtreler diff --git a/docs/user-manuals/tr/getting-started/installation.md b/docs/user-manuals/tr/getting-started/installation.md index 77efae6..ae2d905 100644 --- a/docs/user-manuals/tr/getting-started/installation.md +++ b/docs/user-manuals/tr/getting-started/installation.md @@ -28,7 +28,7 @@ go install github.com/HodeTech/leakwatch@latest İkili dosya `$(go env GOPATH)/bin` dizinine yerleştirilir. Bu dizinin `PATH` değişkeninde olduğundan emin olun. :::note -`go install` her zaman en son etiketli sürümü getirir. Belirli bir sürüme sabitlemek için `@latest` yerine `@v1.5.0` gibi bir etiket kullanın. +`go install` her zaman en son etiketli sürümü getirir. Belirli bir sürüme sabitlemek için `@latest` yerine güncel sürüm etiketi olan `@v1.8.0` değerini kullanın. ::: ## Docker @@ -47,8 +47,8 @@ Kullanılabilir etiketler: | Etiket | Açıklama | |--------|----------| | `:latest` | En son sürüm | -| `:v1.5.0` | Tam sürüm sabitleme | -| `:v1.5` | Küçük sürüm sabitleme (yama sürümlerini takip eder) | +| `:v1.8.0` | Güncel sürüme tam sabitleme | +| `:v1.7` | Güncel küçük sürüme sabitleme (yama sürümlerini takip eder) | Taramak istediğiniz dizini konteyner içindeki `/scan` dizinine bağlayın. Bayraklar ve seçenekler yerel ikili dosyayla tamamen aynı şekilde çalışır — tam liste için [CLI Referansı](#/reference/cli-reference) sayfasına bakın. @@ -86,7 +86,7 @@ leakwatch version Beklenen çıktı: ```text -leakwatch v1.5.0 (commit: a3f9c12, built: 2026-05-10T08:22:00Z) +leakwatch v1.8.0 (commit: 1a2b3c4, built: 2026-08-11T12:00:00Z) ``` Komut bulunamazsa kurulum dizininin `PATH` değişkeninde olup olmadığını kontrol edin. diff --git a/docs/user-manuals/tr/getting-started/introduction.md b/docs/user-manuals/tr/getting-started/introduction.md index 0ae49c5..62fbd00 100644 --- a/docs/user-manuals/tr/getting-started/introduction.md +++ b/docs/user-manuals/tr/getting-started/introduction.md @@ -13,8 +13,8 @@ Go ile yazılmıştır, çalışma zamanı bağımlılığı olmayan tek bir sta Tek bir commit'te sızan bir kimlik bilgisi — sonradan silinse bile — Git geçmişinde sonsuza dek erişilebilir kalabilir ve push edildikten dakikalar sonra istismar edilebilir. Leakwatch, bu sırları erken yakalamak ve hangilerinin *gerçekten tehlikeli* olduğunu söylemek için tasarlanmıştır: -- **Geniş tespit** — bulut sağlayıcılarını, yapay zekâ API'lerini, ödeme platformlarını, veritabanlarını, mesajlaşma araçlarını ve daha fazlasını kapsayan 64 yerleşik dedektör; ayrıca kendi YAML özel kurallarınız. -- **Yalnızca tespit değil, doğrulama** — 54 dedektör türü için Leakwatch, bulunan bir sırrın *hâlâ etkin* olup olmadığını sağlayıcıya kontrollü, salt-okunur bir çağrı yaparak teyit edebilir. Etkin olduğu doğrulanmış bir anahtar bir olaydır; etkin olmayan bir anahtar ise gürültüdür. +- **Geniş tespit** — bulut sağlayıcılarını, yapay zekâ API'lerini, ödeme platformlarını, veritabanlarını, mesajlaşma araçlarını, yapılandırılmış config dosyalarını ve daha fazlasını kapsayan 65 yerleşik dedektör; ayrıca kendi YAML özel kurallarınız. +- **Dürüst doğrulama kabiliyeti** — 39 dedektör türü doğrudan canlı kontrole sahiptir, 9'u güvenilir issuer/bölge/eşlik eden bağlam gerektirir, 6'sı çevrimdışı format doğrular. Registry sayısı hiçbir zaman canlı kapsam gibi sunulmaz. - **Çok sayıda kaynak** — yerel dosya sistemi, eksiksiz bir Git geçmişi, bir OCI/Docker imajı, AWS S3, Google Cloud Storage ve Slack mesajları. - **CI-uyumlu çıktı** — JSON, SARIF (GitHub Code Scanning için), CSV, renklendirilmiş bir terminal tablosu ve satır içi GitHub Actions ek açıklamaları. - **Tasarımı gereği sır-güvenli** — bulunan sırlar varsayılan olarak maskelenir ve asla loglanmaz, önbelleğe alınmaz veya diske yazılmaz. @@ -49,8 +49,8 @@ Leakwatch'ı kullanmak için bu hattı anlamanız gerekmez — ancak taramaları Beklentileri doğru belirlemek için: - Git geçmişini yeniden yazmaz veya sırları sizin için **kaldırmaz** — onları bulup raporlar ve (`--remediation` ile) nasıl döndüreceğinizi söyler. -- Slack taraması yalnızca **mesaj metnini** kapsar; yüklenen dosyaların *içeriğini* taramak uygulanmamıştır. -- Doğrulama, birçok sır türü için mevcuttur ancak hepsi için değil — 10 dedektör türü (JWT'ler ve genel API anahtarları gibi) güvenli biçimde doğrulanamaz ve her zaman doğrulanmamış olarak raporlanır. +- Slack varsayılan olarak mesaj metnini tarar; boyutu sınırlı metin benzeri ekler `--include-files` ve `files:read` kapsamıyla isteğe bağlıdır. +- Doğrulama, birçok sır türü için mevcuttur ancak hepsi için değil — 11 dedektör türü (JWT'ler ve genel API anahtarları gibi) güvenli biçimde doğrulanamaz ve her zaman doğrulanmamış olarak raporlanır. ## Sonraki adımlar diff --git a/docs/user-manuals/tr/getting-started/quick-start.md b/docs/user-manuals/tr/getting-started/quick-start.md index 7c52334..4947421 100644 --- a/docs/user-manuals/tr/getting-started/quick-start.md +++ b/docs/user-manuals/tr/getting-started/quick-start.md @@ -62,7 +62,7 @@ Tablodaki her satır (veya JSON'daki her nesne) bir bulguyu temsil eder. Temel a | **STATUS** | Doğrulama sonucu: `verified_active`, `verified_inactive`, `unverified` veya `verify_error` | | **REMEDIATION** | Döndürme/iptal rehberi başlığı, veya `--remediation` geçilmediğinde `-` | -`verified_active` durumu, Leakwatch'ın sağlayıcıya salt-okunur bir API çağrısı yaparak sırrın hâlâ etkin olduğunu doğruladığı anlamına gelir. **Her `verified_active` bulgusunu açık bir olay olarak değerlendirin.** +`verified_active` durumu, Leakwatch'ın sözleşmeye uygun, yıkıcı olmayan bir sağlayıcı kontrolüyle sırrın hâlâ etkin olduğunu doğruladığı anlamına gelir. **Her `verified_active` bulgusunu açık bir olay olarak değerlendirin.** ## Yaygın tarama seçenekleri diff --git a/docs/user-manuals/tr/output/output-formats.md b/docs/user-manuals/tr/output/output-formats.md index 5fc5a9f..6f95202 100644 --- a/docs/user-manuals/tr/output/output-formats.md +++ b/docs/user-manuals/tr/output/output-formats.md @@ -52,6 +52,10 @@ leakwatch scan fs ./src --format json --output findings.json `id`, düz 32 karakterlik küçük harfli bir hex dizesi olarak gösterilen, deterministik ve kısaltılmış bir SHA-256 değeridir — bir UUID **değildir** ve hiçbir zaman tire içermez. Nasıl hesaplandığı için [Nasıl Çalışır](#/getting-started/how-it-works) sayfasına bakın. +`entropy` hesaplanmadığında atlanır; hesaplanan `0.0` değeri sıfır olarak yazılır. Zaman damgası olmayan kütüphane üretimi bulgularda `detected_at`, Go'nun 1. yıl sıfır zamanı yerine atlanır. Normal CLI bulguları zaman damgasını ayarlar. + +Sağlayıcı tarafından doğrulanmış kimlik, kapsam ve sona erme meta verisi mevcut olduğunda `verification.extra_data` altında görünür. Değerler sınırlı ve şema doğrulamalıdır; alanın olmaması token'ın izni veya sona ermesi olmadığı değil, sağlayıcının güvenli meta veri sunmadığı anlamına gelir. + `--remediation` de ayarlandığında her bulgunun içine iç içe bir `"remediation"` nesnesi yerleştirilir. Bkz. [Düzeltme Rehberi](#/output/remediation). ## SARIF @@ -110,7 +114,7 @@ e6fa909746d7d5242309b64c33209fa9,aws-access-key-id,high,AKIA****K7NP,,config/aws ## Tablo -`table` formatı, insan tarafından okunabilir sekme hizalı bir tablo yazar; sonuçların hızlı görsel taramasını istediğiniz etkileşimli terminal oturumları için en uygun formattır. +`table` formatı; CJK, birleşen işaretler ve emoji için doğru görüntü genişliği dahil, terminal hücrelerine göre hizalanmış insan tarafından okunabilir bir tablo yazar. Sonuçların hızlı görsel taramasını istediğiniz etkileşimli terminal oturumları için en uygun formattır. **Sütunlar:** @@ -118,9 +122,9 @@ e6fa909746d7d5242309b64c33209fa9,aws-access-key-id,high,AKIA****K7NP,,config/aws SEVERITY | DETECTOR | FILE | LINE | REDACTED | STATUS | REMEDIATION ``` -`LINE` sütunu, satır numarası mevcut olmadığında (örneğin bir Slack veya konteyner imajı bulgusu için) `-` gösterir. `REMEDIATION` sütunu her zaman bulunur ve `--remediation` ayarlanmadıkça `-` gösterir. `--show-raw` ayarlandığında da, sona bir `RAW` sütunu eklenir. Tablonun altına bir özet satırı yazdırılır (örn. `Found 3 secrets (1 critical, 2 high).`). +`LINE` sütunu, satır numarası mevcut olmadığında (örneğin bir Slack veya konteyner imajı bulgusu için) `-` gösterir. `REMEDIATION` sütunu her zaman bulunur ve `--remediation` ayarlanmadıkça `-` gösterir. `--show-raw` ayarlandığında da, sona bir `RAW` sütunu eklenir. Yalnızca tablo çıktısında bu değer geri çevrilebilir, Go-tırnaklı bir dize olarak gösterilir: `\\n`, `\\t` ve `\\x1b` gibi kaçışlar terminal kontrol baytlarını etkisiz tutarken `strconv.Unquote` özgün baytları eksiksiz geri yükleyebilir. JSON ve CSV ham değerleri mevcut makine-okunur gösterimlerini korur. Tablonun altına bir özet satırı yazdırılır (örn. `Found 3 secrets (1 critical, 2 high).`). -Saldırgan tarafından etkilenebilecek alanlar (dedektör ID'si, dosya yolu, maskelenmiş değer), terminale yazılmadan önce kontrol karakterlerinden ve ANSI kaçış dizilerinden arındırılır; böylece kötü amaçlı biçimlendirilmiş bir dosya adı veya özel kural eşleşmesi terminal oturumunuza kaçış dizisi enjekte edemez. +Saldırgan tarafından etkilenebilecek alanlar (dedektör ID'si, dosya yolu, maskelenmiş değer), terminale yazılmadan önce kontrol karakterlerinden ve ANSI kaçış dizilerinden arındırılır. Unicode çift yönlü metin (bidi) kontrol karakterleri görünür `\\uXXXX` kod noktaları olarak yazılır; böylece varlıkları gizlenmeden görsel sıralama saldırıları önlenir. CJK, birleşen işaretler ve emoji ZWJ dizileri dahil olağan uluslararası metin korunur. **ANSI rengi**, `SEVERITY` sütununa otomatik olarak uygulanır, ancak yalnızca dört koşulun tamamı sağlandığında: diff --git a/docs/user-manuals/tr/reference/cli-reference.md b/docs/user-manuals/tr/reference/cli-reference.md index 5c9703c..0c95f69 100644 --- a/docs/user-manuals/tr/reference/cli-reference.md +++ b/docs/user-manuals/tr/reference/cli-reference.md @@ -25,7 +25,7 @@ leakwatch version ``` ```text -leakwatch v1.5.0 (commit: a3f9c12, built: 2026-05-10T08:22:00Z) +leakwatch v1.8.0 (commit: 1a2b3c4, built: 2026-08-11T12:00:00Z) ``` ## `leakwatch init` @@ -67,6 +67,8 @@ Aşağıdaki bayraklar **tüm** `scan` alt komutlarında kullanılabilir. | `--exclude-detectors` | — | — | Bu çalıştırma için hariç tutulacak dedektör kimlikleri (örn. `aws-access-key-id,generic-api-key`). Tekrarlanabilir veya virgülle ayrılabilir; yapılandırma dosyasındaki `filter.exclude-detectors` ile birleştirilir (onun yerine geçmez). | | `--no-verify` | — | `false` | Canlı sır doğrulamasını devre dışı bırak. Giden API çağrısı yapılmaz. | | `--only-verified` | — | `false` | Yalnızca canlı doğrulama ile etkin olduğu teyit edilen bulguları raporla. | +| `--verifier-origin ` | — | — | Bağlam gerektiren bir dedektörü operatörce güvenilen HTTPS origin'ine bağla. Tekrarlanabilir; yalnız komut satırında kabul edilir. | +| `--grafana-instance-url ` | — | — | Güvenilir Grafana origin'ini sağlayan geriye uyumlu alias. | | `--min-severity` | — | `low` | Çıktıya dahil edilecek minimum önem derecesi: `low`, `medium`, `high` veya `critical`. Tanınmayan bir değer (örn. bir yazım hatası) sessizce kabul edilmek yerine kesin bir hataya yol açar. | | `--remediation` | — | `false` | Her bulguya düzeltme rehberi (dönüşüm/iptal adımları) ekle. | @@ -252,7 +254,8 @@ Konumsal argüman yoktur. | `--exclude-channels ` | — | Atlanacak kanal adları veya kimliklerinin virgülle ayrılmış listesi. | | `--since ` | — | Yalnızca bu tarihten sonra gönderilen mesajları tara. | | `--include-dms` | `false` | Doğrudan mesajları dahil et (ek OAuth kapsamları gerektirir). | -| `--rate-limit ` | `1` | Saniye başına maksimum Slack API isteği. | +| `--include-files` | `false` | Boyutu sınırlı metin benzeri ekleri indir ve tara. `files:read` gerektirir. | +| `--rate-limit ` | `0` | Saniyelik isteğe bağlı işlem-başı istek tavanı. Sıfır, Slack için güvenli işlem-özel varsayılanları korur. | `scan slack` komutunda (diğer her tarama alt komutunun aksine) `--exclude` yol-kalıbı bayrağı yoktur — bunun yerine tüm kanalları atlamak için `--exclude-channels` kullanın. diff --git a/docs/user-manuals/tr/reference/exit-codes.md b/docs/user-manuals/tr/reference/exit-codes.md index e9a81ba..9b59f98 100644 --- a/docs/user-manuals/tr/reference/exit-codes.md +++ b/docs/user-manuals/tr/reference/exit-codes.md @@ -26,7 +26,7 @@ Leakwatch, CI pipeline'larının ve kabuk betiklerinin çıktıyı ayrıştırma Bu, çıkış kodu `0`'ın "mevcut filtre ayarlarınızla eşleşen bulgu yok" anlamına geldiği anlamına gelir — kod tabanının hiçbir sır içermediği değil. :::warn -`--only-verified` altında temiz `0` çıkışı, kod tabanının sırdan arındırılmış olduğunu garanti etmez. Doğrulamanın mevcut olmadığı sır türleri (10 dedektör türü) her zaman doğrulanmamış olarak raporlanır ve `--only-verified` tarafından bastırılır. Tam kapsam için `--only-verified` ile birlikte ayrı bir filtresiz tarama yapın. +`--only-verified` altında temiz `0` çıkışı, kod tabanının sırdan arındırılmış olduğunu garanti etmez. Doğrulamanın mevcut olmadığı sır türleri (11 dedektör türü) her zaman doğrulanmamış olarak raporlanır ve `--only-verified` tarafından bastırılır. Tam kapsam için `--only-verified` ile birlikte ayrı bir filtresiz tarama yapın. ::: ## Kabuk betiklerinde çıkış kodlarını kullanma diff --git a/docs/user-manuals/tr/scanning/slack.md b/docs/user-manuals/tr/scanning/slack.md index a76f32e..7d70ba3 100644 --- a/docs/user-manuals/tr/scanning/slack.md +++ b/docs/user-manuals/tr/scanning/slack.md @@ -1,14 +1,14 @@ --- title: "Slack Çalışma Alanı" -description: "Slack kanal ve DM mesaj metinlerini sızan sırlara karşı tarayın." +description: "Slack mesajlarını ve isteğe bağlı metin eklerini sızan sırlara karşı tarayın." --- # Slack Çalışma Alanı -Geliştiriciler çoğu zaman kimlik bilgilerini sohbet üzerinden paylaşır — hızlı bir test için bir kanala yapıştırılan token, DM ile gönderilen parola ya da bir olay başlığında söz edilen API anahtarı. `leakwatch scan slack`, Slack çalışma alanınızdaki mesaj metinlerini okur ve bulduğu sırları işaretler. +Geliştiriciler çoğu zaman kimlik bilgilerini sohbet üzerinden paylaşır — hızlı bir test için bir kanala yapıştırılan token, DM ile gönderilen parola ya da olay başlığına yüklenen bir yapılandırma dosyası. `leakwatch scan slack`, Slack çalışma alanınızdaki mesaj metinlerini ve açıkça etkinleştirildiğinde metin benzeri dosya eklerini okur. -:::warn -Leakwatch yalnızca **mesaj metnini** tarar. Yüklenen dosyaların (ekler, snippet'ler) içeriğini taramak uygulanmamıştır. Yalnızca mesajların metin gövdesi analiz edilir. +:::note +Dosya eki taraması isteğe bağlıdır. `--include-files` kullanın ve `files:read` kapsamını verin; bayrak olmadan Leakwatch hiçbir eki indirmez. ::: ## Temel kullanım @@ -34,10 +34,15 @@ Bot token'ı, aşağıdaki OAuth kapsamlarına sahip bir Slack uygulamasıyla il | Kapsam | Amaç | |--------|------| +| `channels:read` | Genel kanalları listele. | | `channels:history` | Botun katıldığı genel kanallardaki mesajları oku. | +| `groups:read` | Özel kanalları listele. | | `groups:history` | Botun katıldığı özel kanallardaki mesajları oku. | +| `im:read` | Doğrudan mesaj konuşmalarını listele (yalnızca `--include-dms` ile gerekli). | | `im:history` | Doğrudan mesajları oku (yalnızca `--include-dms` ile gerekli). | +| `mpim:read` | Grup doğrudan mesaj konuşmalarını listele (yalnızca `--include-dms` ile gerekli). | | `mpim:history` | Grup doğrudan mesajlarını oku (yalnızca `--include-dms` ile gerekli). | +| `files:read` | Dosya meta verisini ve içeriğini oku (yalnızca `--include-files` ile gerekli). | ## Bayraklar @@ -50,7 +55,8 @@ Bot token'ı, aşağıdaki OAuth kapsamlarına sahip bir Slack uygulamasıyla il | `--exclude-channels` | string | — | Atlanacak kanal adlarının virgülle ayrılmış listesi. | | `--since` | string (YYYY-MM-DD) | — | Bu tarihte veya sonrasında gönderilen mesajları tara. | | `--include-dms` | bool | `false` | Doğrudan mesajları ve grup DM'lerini de tara. | -| `--rate-limit` | float | `1` | Saniye başına maksimum Slack API istek sayısı. | +| `--include-files` | bool | `false` | Boyutu sınırlı metin benzeri dosya eklerini indir ve tara. `files:read` gerektirir. | +| `--rate-limit` | float | `0` | İsteğe bağlı, işlem başına saniyelik Slack istek üst sınırı. Sıfır, güvenli işlem varsayılanlarını korur: history `1/60`, liste/dosya/indirme sınırları daha yüksektir. | ### Ortak tarama bayrakları @@ -59,7 +65,7 @@ Bot token'ı, aşağıdaki OAuth kapsamlarına sahip bir Slack uygulamasıyla il | `--format` | `-f` | `json` | Çıktı biçimi: `json`, `sarif`, `csv`, `table`, `github`. | | `--output` | `-o` | stdout | Sonuçları stdout yerine bu dosyaya yaz. | | `--concurrency` | `-c` | CPU sayısı | Eşzamanlı çalışan sayısı. | -| `--max-file-size` | — | `10485760` (10 MB) | Dahili parça boyutu sınırı (bayt). | +| `--max-file-size` | — | `10485760` (10 MB) | Tarama için belleğe alınan en büyük ek boyutu (bayt). | | `--show-raw` | — | `false` | Çıktıda ham sır değerini göster. | | `--exclude-detectors` | — | — | Bu çalıştırma için hariç tutulacak dedektör kimlikleri. Tekrarlanabilir; `filter.exclude-detectors` ile birleştirilir. | | `--no-verify` | — | `false` | Sır doğrulamasını devre dışı bırak. | @@ -94,10 +100,18 @@ leakwatch scan slack \ --include-dms ``` -Büyük çalışma alanlarında Slack hız sınırı hatalarını önlemek için API istek hızını düşürün: +Dosya başına bellek sınırını düşürerek metin benzeri ekleri dahil edin: ```bash -leakwatch scan slack --rate-limit 10 --format table +leakwatch scan slack \ + --include-files \ + --max-file-size 5242880 +``` + +API istek hızını yalnız yayımlanmış katmanı izin veren Marketplace/dahili bir uygulama için artırın: + +```bash +leakwatch scan slack --rate-limit 0.8 --format table ``` Yalnızca doğrulanmış aktif bulguları bir JSON dosyasına kaydedin: @@ -120,13 +134,16 @@ Slack taramasından elde edilen her bulgu mesaj ve kanal meta verisi içerir: | `message_user` | Mesaj yazarının Slack kullanıcı kimliği. Slack bulguları için `author` alanı yoktur. | | `message_ts` | Slack mesaj zaman damgası (benzersiz mesaj kimliği). | | `thread_ts` | Yalnızca bulgu bir ileti dizisi yanıtındaysa bulunan, üst mesajın zaman damgası. | +| `file_path` | Ekten gelen bulgularda bulunan sentetik `slack//` yolu. | ## Performans değerlendirmeleri -Slack API istekleri, Slack tarafından uygulanan hız sınırlarına tabidir. `--rate-limit` bayrağı (varsayılan saniyede `1` istek), Leakwatch'ın istekleri ne kadar agresif yapacağını kontrol eder. Cömert hız sınırlarına sahip bir çalışma alanında daha hızlı bir tarama için dikkatli bir şekilde artırın, ya da `429 Too Many Requests` hatası görüyorsanız daha da düşürün. +Slack API sınırları yöntem, çalışma alanı ve uygulama başına uygulanır. Bu nedenle Leakwatch bağımsız limiter kovaları kullanır: `conversations.history`, yeni dağıtılan Marketplace dışı uygulamalar için varsayılan dakikada bir istektir; `conversations.list` Tier 2'yi (20+/dakika), `files.info` Tier 4'ü (100+/dakika) izler ve ek indirmelerinin ayrı, ihtiyatlı bir 100/dakika istemci sınırı vardır. `--rate-limit` her kovayı aynı işlem-başı üst sınırla açıkça geçersiz kılar; Tier 3 erişimli Marketplace/dahili uygulamalar bunu bilinçli olarak artırabilir. Slack `429 Too Many Requests` ile yanıt verdiğinde, Leakwatch `Retry-After` başlığına otomatik olarak uyar ve taramayı tamamen başarısız kılmak yerine isteği yeniden dener. +Dosya meta verisi ve indirmeler birbirlerinin veya history kapasitesini tüketmek yerine ayrı limiter kovaları kullanır. Leakwatch yalnızca Slack'e ait HTTPS indirme URL'lerini kabul eder, ek chunk'larını doğrudan backpressure ile aktarır, en fazla `--max-file-size` kadar veriyi belleğe alır, MIME boş ya da yanıltıcı olsa bile geçersiz UTF-8/ikili içeriği reddeder ve aynı Slack dosya kimliğini mesajlar arasında tekilleştirir. + Her çalıştırmada tüm çalışma alanını taramak yerine belirli kanalları hedeflemek için `--channels` kullanın. Mesajları artımlı biçimde taramak için `--since` ile birleştirin. ## Çıkış kodları diff --git a/docs/user-manuals/tr/verification/how-verification-works.md b/docs/user-manuals/tr/verification/how-verification-works.md index 3601d57..39c7cd2 100644 --- a/docs/user-manuals/tr/verification/how-verification-works.md +++ b/docs/user-manuals/tr/verification/how-verification-works.md @@ -14,15 +14,19 @@ Tarama motoru bulguları topladıktan sonra doğrulayıcı havuzu onları işlem - Bir doğrulayıcı mevcutsa çalışır ve bir durum döndürür. - O dedektör türü için kayıtlı bir doğrulayıcı yoksa bulgu değiştirilmeden `unverified` durumuyla geçer. -## İki doğrulama modu +## Üç doğrulama modu -Tüm sırlar aynı şekilde doğrulanamaz. Leakwatch, her kimlik bilgisi türü için güvenli olan yaklaşıma göre iki farklı yöntem kullanır. +Tüm sırlar aynı şekilde doğrulanamaz. Leakwatch doğrudan canlı kontrolleri, güvenilir veya eşlik eden bağlam gerektiren kontrolleri ve çevrimdışı format doğrulamasını birbirinden ayırır. Bu nedenle registry kaydı canlı kabiliyetin kanıtı sayılmaz. ### Canlı API doğrulaması -48 dedektör türü için Leakwatch, sağlayıcıya **kontrollü, salt-okunur bir API çağrısı** yapar — örneğin AWS anahtarları için `sts:GetCallerIdentity`, GitHub token'ları için `GET /user`. Çağrı yalnızca kimliği doğrulamak için gereken minimum uç noktayı kullanır; hiçbir zaman veri değiştirmez, kaynak oluşturmaz veya faturalandırma olayı tetiklemez. +39 dedektör türü için Leakwatch normal üretim yolunda **kontrollü, yıkıcı olmayan bir sağlayıcı kontrolü** yapabilir — örneğin AWS anahtarları için `sts:GetCallerIdentity`, OpenAI anahtarları için sabit sağlayıcı kimlik uç noktası. Çağrı yalnızca kimliği doğrulamak için gereken minimum uç noktayı kullanır; hiçbir zaman veri değiştirmez veya kaynak oluşturmaz, ancak sağlayıcı kotası tüketebilir. -Sağlayıcı başarılı bir yanıt döndürürse bulgu `verified_active` olarak işaretlenir. Sağlayıcı kimlik bilgisini reddederse (örneğin HTTP 401 veya kapsamlı-anahtar hatalarını "inaktif" içinde birleştiren bir sağlayıcı için HTTP 403 ile) bulgu `verified_inactive` olarak işaretlenir. Birkaç doğrulayıcı (örneğin SendGrid), kapsamı daraltılmış ama geçerli bir anahtardan kaynaklanan bir 403'ü gerçek bir reddediliş yanıtından ayırt eder ve bu durumda yine `verified_active` bildirir — sağlayıcıya özgü notlar için [Doğrulama Kapsamı](#/verification/verification-coverage) sayfasına bakın. +Sağlayıcı sözleşmeye uygun başarılı bir yanıt döndürürse bulgu `verified_active` olarak işaretlenir. Bir bulgu yalnızca sağlayıcı yanıtı ilgili doğrulayıcı sözleşmesine göre kesin olduğunda `verified_inactive` olur. İzin reddi ve belirsiz yanıtlar `verify_error` olarak kalır; örneğin SendGrid yalnız HTTP `401` yanıtını inaktif sayar, `403` ise sonuçsuzdur. + +### Güvenilir veya eşlik eden bağlam gerekli + +Kayıtlı dokuz implementasyon çıplak dedektör bulgusundan güvenli canlı istek yapamaz. Auth0, GitLab, Grafana, GitHub/GHES, Datadog ve Snyk güvenilir issuer/site/API origin'i gerektirir. Twilio bulgusu açıkça eşleşen API Key Secret ile gizli olmayan Key SID'yi içerir, ancak yine de güvenilir bölgesel origin gerekir. Shopify için operatörce güvenilen mağaza origin'i zorunludur. Bu hedefleri yalnızca tekrarlanabilir `--verifier-origin detector-id=https://host` komut satırı bayrağıyla verin (`--grafana-instance-url` Grafana için korunmuştur). Proje yapılandırması, ortam değişkenleri, repo URL'leri ve token iddiaları doğrulama hedefi seçemez. Açık bağlam olmadan Leakwatch istek göndermez ve `unverified` döndürür. ### Yalnızca format doğrulaması @@ -49,7 +53,7 @@ Leakwatch çıktısındaki her bulgu dört durumdan birini taşır: |-------|-------|----------------| | `verified_active` | Sırrın sağlayıcı tarafından canlı olduğu teyit edildi. | Aktif bir olay olarak ele alın. Hemen döndürün. | | `verified_inactive` | Sağlayıcı kimlik bilgisini reddetti. | Muhtemelen zaten döndürülmüş. Bağlamı gözden geçirin ve kapatın. | -| `unverified` | Bu tür için doğrulayıcı yok, format doğrulaması sonuç vermedi veya doğrulama devre dışı bırakıldı. | Manuel olarak inceleyin; risk bağlama göre belirlenir. | +| `unverified` | Doğrulayıcı yoktur, gerekli bağlam eksiktir, yalnız-format doğrulayıcısı canlılığı kanıtlayamaz veya doğrulama devre dışıdır. | Manuel olarak inceleyin; risk bağlama göre belirlenir. | | `verify_error` | Doğrulayıcı çalıştı ancak ağ hatası, zaman aşımı veya beklenmedik yanıtla karşılaştı. | Potansiyel olarak aktif kabul edin. Yeniden deneyin veya manuel olarak inceleyin. | ## Doğrulama motoru @@ -59,8 +63,8 @@ Doğrulama, tarama çalışan havuzundan yalıtılmış ayrı bir eşzamanlı ç | Ayar | Varsayılan | Yapılandırma anahtarı | |------|-----------|----------------------| | Çalışan sayısı | 4 | `verification.concurrency` | -| Global hız sınırı | 10 istek/saniye | `verification.rate-limit` | -| İstek başına zaman aşımı | 10 sn | `verification.timeout` | +| Global + dedektör başına istek tavanları | Her biri 10 istek/saniye | `verification.rate-limit` | +| Bulgu başına tam doğrulama operasyonu zaman aşımı | 10 sn | `verification.timeout` | Her üç değer de `.leakwatch.yaml` içindeki `verification:` bloğu altında ayarlanabilir: @@ -68,14 +72,16 @@ Her üç değer de `.leakwatch.yaml` içindeki `verification:` bloğu altında a verification: enabled: true concurrency: 4 - rate-limit: 10.0 # global, saniye başına istek sayısı + rate-limit: 10.0 # global ve dedektör başına, saniye başına istek sayısı timeout: 10s ``` :::tip -Yüzlerce bulgu tetikleyen bir depoyu tarıyorsanız `rate-limit` değerini 5'e düşürmeyi veya `--only-verified` etkinleştirmeyi düşünün; bu, doğrulanmış-aktif kümesini küçük ve uygulanabilir tutar. +Yüzlerce bulgu tetikleyen depolarda sağlayıcı için güvenli hız tavanını koruyun; daha yumuşak trafik gerekiyorsa `concurrency` değerini düşürün. `--only-verified` yalnız çıktı filtrelemesini değiştirir, doğrulama isteği sayısını azaltmaz. ::: +Admission yalnızca gerçek bir sağlayıcı isteğinin hemen öncesinde yapılır. Yalnız-format ve eksik-bağlam sonuçları sınırlayıcı kapasitesi tüketmez. Dedektör başına kova o dedektörün kendi istek hızını, paylaşılan global kova ise toplam trafiği sınırlar; bu yapı sağlayıcılar arasında adil zamanlama garantisi vermez. + ## Komut satırından doğrulamayı kontrol etme `--no-verify` ile **doğrulamayı tamamen devre dışı bırakın** (ya da yapılandırmada `verification.enabled: false` ayarlayın). Her bulgu `unverified` olarak geçer. Bunu çevrimdışı veya hava boşluklu ortamlar için ya da herhangi bir sağlayıcı API'sine dokunmadan mümkün olan en hızlı taramayı istediğinizde kullanın. diff --git a/docs/user-manuals/tr/verification/verification-coverage.md b/docs/user-manuals/tr/verification/verification-coverage.md index 2c52192..5d7ac8d 100644 --- a/docs/user-manuals/tr/verification/verification-coverage.md +++ b/docs/user-manuals/tr/verification/verification-coverage.md @@ -1,28 +1,25 @@ --- title: "Doğrulama Kapsamı" -description: "64 yerleşik dedektörün hangilerinin canlı doğrulandığı, yalnızca format doğrulandığı veya doğrulanamaz olduğu ve bunun önceliklendirme açısından ne anlama geldiği." +description: "65 yerleşik dedektörün hangilerinin canlı doğrulandığı, bağlam gerektirdiği, yalnızca format doğruladığı veya doğrulanamaz olduğu ve bunun önceliklendirme açısından ne anlama geldiği." --- # Doğrulama Kapsamı -Leakwatch 64 yerleşik dedektör ve 54 doğrulayıcı ile gelir; bu, **%84,4** kapsama oranı sağlar (64 dedektör türünden 54'ünde bir tür doğrulama mevcuttur — canlı ya da yalnızca format). Bu sayfa, çıktınızda ne beklemeniz gerektiğini bilmeniz için her dedektörü doğrulama durumuna göre eşler. +Leakwatch **65 yerleşik dedektör** ve kayıtlı 54 doğrulayıcı implementasyonuyla gelir. Registry kaydı canlı kabiliyetle aynı şey değildir: **39** dedektör normal üretim yolunda canlı kontrol yapabilir, **9** dedektör güvenilir operatör veya eşlik eden bağlam gerektirir, **6** dedektör yalnızca çevrimdışı format doğrular ve **11** dedektörün doğrulayıcısı yoktur. Bu sayfa her dedektörü gerçek doğrulama sözleşmesine göre eşler. -## Canlı doğrulanan (48 dedektör türü) +## Canlı doğrulanan (39 dedektör türü) -Bu türler için Leakwatch, sağlayıcıya kontrollü, salt-okunur bir API çağrısı yapar ve `verified_active` ya da `verified_inactive` döndürür. Hiçbir veri oluşturulmaz veya değiştirilmez; çağrı, kimliği doğrulamak için gereken minimum uç noktayı kullanır. +Bu türler için Leakwatch normal üretim yolunda kontrollü, yıkıcı olmayan bir sağlayıcı kontrolü yapabilir. Sözleşmeye uygun başarı `verified_active` döndürebilir; yalnızca doğru issuer üzerindeki kesin kimlik doğrulama reddi `verified_inactive` döndürebilir. Belirsiz yanıtlar `verify_error` kalır. | Dedektör türü | Sağlayıcı | |--------------|----------| | `aws-access-key-id` | AWS STS (`GetCallerIdentity`) | -| `github-token` | GitHub REST API | -| `github-oauth-token` | GitHub REST API | -| `gitlab-pat` | GitLab REST API (token ile birlikte yakalanan kendi barındırılan bir GitLab sunucusu varsa onu hedefler; yoksa gitlab.com'a geri döner) | | `slack-token` | Slack Web API | | `openai-api-key` | OpenAI API | | `anthropic-api-key` | Anthropic API | | `deepseek-api-key` | DeepSeek API | | `huggingface-token` | Hugging Face API | -| `sendgrid-api-key` | SendGrid Web API (kapsamı daraltılmış/kısıtlı bir anahtardan gelen `403` yanıtı, anahtarın kendisi geçerli olduğundan inaktif değil `verified_active` olarak değerlendirilir — yalnızca `401` `verified_inactive` olarak eşlenir) | +| `sendgrid-api-key` | SendGrid Web API (`401` inaktiftir; izin reddi dâhil `403`, `verify_error` olarak kalır) | | `mailgun-api-key` | Mailgun API (doğru AB veya ABD bölgesel uç noktasını otomatik olarak tespit edip çağırır) | | `postmark-server-token` | Postmark API | | `stripe-api-key-live` | Stripe API | @@ -41,27 +38,39 @@ Bu türler için Leakwatch, sağlayıcıya kontrollü, salt-okunur bir API çağ | `telegram-bot-token` | Telegram Bot API | | `sentry-token` | Sentry API | | `pagerduty-api-key` | PagerDuty API | -| `newrelic-api-key` | New Relic API | -| `grafana-api-key` | Grafana API | -| `datadog-api-key` | Datadog API | -| `snyk-api-key` | Snyk API | -| `twilio-api-key` | Twilio API (API Key Secret'ıyla eşleştirilmiş API Key SID ile kimlik doğrular; eşleştirilmiş sır olmadan sonuç, asla yanlış bir `verified_inactive` değil, `unverified` olur) | +| `newrelic-api-key` | New Relic NerdGraph (resmî ABD/AB uçlarında sınırlı fallback; yalnızca iki bölge de `401` döndürürse inaktif) | | `doppler-token` | Doppler API | | `launchdarkly-sdk-key` | LaunchDarkly API | | `sonarcloud-token` | SonarCloud API | -| `shopify-access-token` | Shopify Admin API | | `notion-token` | Notion API | | `linear-api-key` | Linear API | | `figma-pat` | Figma REST API | | `airtable-pat` | Airtable API | | `okta-api-token` | Okta API (token ile birlikte yakalanan organizasyon alan adını hedefler) | -| `auth0-management-token` | Auth0 Management API (token'ın kendi JWT `iss` iddiasından çözülen kiracıyı hedefler) | | `databricks-token` | Databricks REST API (token ile birlikte yakalanan çalışma alanı ana bilgisayarını çağırır) | | `bitbucket-app-password` | Bitbucket REST API | -| `supabase-service-key` | Supabase API | +| `supabase-service-key` | Supabase Management API (`sbp_` kişisel erişim token'ı; `401` inaktiftir, `403` `verify_error` kalır) | | `infura-api-key` | Infura API | | `teams-webhook` | Microsoft Teams | +## Güvenilir veya eşlik eden bağlam gerektiren (9 dedektör türü) + +Bu implementasyonlar registry'de kayıtlıdır; ancak çıplak bir dedektör bulgusu güvenli issuer seçmek veya doğrulama isteğinde kimlik doğrulamak için yeterli değildir. Gerekli bağlam yoksa Leakwatch güvenli olmayan bir varsayım yapmaz ve `unverified` döndürür. + +Origin'i yalnızca tekrarlanabilir `--verifier-origin detector-id=https://host` komut satırı bayrağıyla verin. Bu yönlendirme girdisi proje yapılandırma dosyaları ve ortam değişkenlerinde özellikle yok sayılır; dolayısıyla taranan içerik kimlik bilgisinin hedefini değiştiremez. Eski `--grafana-instance-url` bayrağı yalnızca Grafana için alias olarak kalır. + +| Dedektör ID | Gerekli bağlam | Üretim davranışı | +|-------------|----------------|------------------| +| `auth0-management-token` | Operatörce güvenilen Auth0 tenant veya özel alan adı origin'i | `--verifier-origin auth0-management-token=https://tenant` ile yapılandırılır. Dedektör tam üç parçalı Management JWT üretir; doğrulanmamış iddialar ve repo URL'leri hedef seçmez. Salt-okunur clients yoklamasında yalnız `401` inaktiftir. | +| `gitlab-pat` | Operatörce güvenilen GitLab.com veya self-managed API origin'i | `--verifier-origin gitlab-pat=https://gitlab.example` ile yapılandırılır. Repo içeriği ve finding metadata'sı hedef seçmez. Güvenli `/api/v4/user` yoklamasını yalnız `glpat-` PAT kullanır; tanınan diğer GitLab token alt türleri `unverified` kalır. Aktif kimlik kanıtından sonra best-effort `/personal_access_tokens/self` çağrısı sınırlı, doğrulanmış ve sıralanmış standart veya GitLab 19.2+ ayrıntılı kapsamları ve ISO sona erme tarihini `verification.extra_data` alanına ekler; erişilemeyen ya da hatalı meta veri aktif kanıtı silmez. `401`, yalnızca katı JSON gövdesi GitLab'ın standart geçersiz-token yanıtıysa inaktiftir; DPoP challenge'ları `verify_error` kalır. | +| `grafana-api-key` | `--grafana-instance-url` ile verilen güvenilir Grafana instance origin'i | Yalnızca doğrulanmış HTTPS instance'ını çağırır. Repo içeriği veya finding metadata hedef seçemez; `401` yalnızca bu güvenilir issuer üzerinde inaktiftir. | +| `twilio-api-key` | Eşleşen API Key SID ve operatörce güvenilen bölgesel API origin'i (US1/IE1/AU1) | `--verifier-origin twilio-api-key=https://api.twilio.com` (veya doğru bölgesel origin) ile yapılandırılır. Tek başına `SK...` SID bulgu değildir; opaque secret yalnızca yakındaki atanmış SID ile bire bir eşleşirse üretilir. Yalnız `401` inaktiftir, izin `403` yanıtı `verify_error` kalır. | +| `shopify-access-token` | Operatörce güvenilen issuer mağaza origin'i | `--verifier-origin shopify-access-token=https://store.myshopify.com` ile yapılandırılır. Finding metadata'sı hedef seçmez. Verifier sabitlenmiş 2026-07 Admin GraphQL mağaza kimliği sorgusunu kullanır; yalnız seçilen mağazadaki `401` inaktiftir. | +| `github-token` | Güvenilir GitHub.com veya GitHub Enterprise Server API origin'i | `--verifier-origin github-token=https://api.github.com` veya GHES origin'iyle yapılandırılır. GHES aynı `ghp_` ve `github_pat_` biçimlerini kullanır; repo metadata'sı issuer seçemez. Issuer sağladığında doğrulanmış kapsam/sayı ve sona erme yanıt başlıkları `verification.extra_data` alanına eklenir. | +| `github-oauth-token` | Güvenilir GitHub.com veya GitHub Enterprise Server API origin'i | `--verifier-origin github-oauth-token=https://api.github.com` veya GHES origin'iyle yapılandırılır. `gho_`/`ghu_`, `/user` kullanır ve aynı güvenli kapsam/sona erme zenginleştirmesini alır; `ghs_` installation repositories endpoint'ini kullanır. Döndürücü exchange nedeniyle `ghr_` istek yapılmadan `unverified` kalır. | +| `datadog-api-key` | Güvenilir Datadog site/API origin'i | Tam resmî site `--verifier-origin datadog-api-key=https://api.datadoghq.com` (veya bölgesel/FED hostu) ile seçilir. Keyfî hostlar reddedilir; yanlış bölgenin reddi iptal kanıtı sayılmaz. | +| `snyk-api-key` | Güvenilir Snyk bölgesel, kamu veya özel API origin'i | `--verifier-origin snyk-api-key=https://api.snyk.io` (veya doğru bölgesel/özel origin) ile yapılandırılır. Yanlış bölgenin reddi iptal kanıtı değildir. Yalnız `401` inaktiftir; plan/izin eksikliğini gösterebilen `403`, `verify_error` kalır. | + ## Yalnızca format doğrulaması (6 dedektör türü) Bu doğrulayıcılar tamamen çevrimdışı çalışır. Hiçbir ağ isteği yapılmaz. Geçerli bir format kimlik bilgisinin aktif olduğunu kanıtlamadığından, altısı da format kontrolünün geçip geçmediğinden bağımsız olarak her zaman `unverified` döndürür. @@ -75,7 +84,7 @@ Bu doğrulayıcılar tamamen çevrimdışı çalışır. Hiçbir ağ isteği yap | `azure-entra-secret` | Format kontrolü | İstemci kimlik bilgisi akışı oturum oluşturur | | `coinbase-api-key` | Karakter kümesi ve uzunluk kontrolü | Coinbase'in eski API'si, dedektörün anahtarla güvenilir biçimde ilişkilendiremeyeceği eşleştirilmiş sırrı gerektiren HMAC-SHA256 istek imzalamayla kimlik doğrular; canlı doğrulama denenmez, böylece gerçek bir anahtar hiçbir zaman yanlışlıkla inaktif olarak raporlanmaz | -## Doğrulanamaz (10 dedektör türü) +## Doğrulanamaz (11 dedektör türü) Bu dedektör türlerinin hiç doğrulayıcısı yoktur. Bunlardan gelen bulgular her zaman `unverified` olur. Bu durum önemsiz oldukları anlamına **gelmez** — tam olarak tespit edilip raporlanırlar — ancak herkese açık bir doğrulama API'si bulunmamakta ya da herhangi bir doğrulama girişimi yan etkiye yol açmaktadır. @@ -91,20 +100,22 @@ Bu dedektör türlerinin hiç doğrulayıcısı yoktur. Bunlardan gelen bulgular | `slack-webhook` | Webhook'un aktif olduğunu doğrulamak mesaj göndermeyi gerektirir | | `hashicorp-vault-token` | Vault token doğrulaması, Vault uç noktasının bilinmesini gerektirir | | `discord-webhook-url` | Bir webhook'un aktif olduğunu doğrulamak, ona bir mesaj göndermeyi gerektirir | +| `structured-config-secret` | Bağlamsal fallback bir secret rolünü belirleyebilir ancak credential sağlayıcısını veya issuer'ı belirleyemez | :::note -"Doğrulanamaz" "bulunamaz" anlamına gelmez. Bu 10 türün tamamı yine de tespit edilir ve çıktınızda görünür. Kimlik bilgisinin canlı olup olmadığını ve döndürülmesi gerekip gerekmediğini belirlemek için manuel inceleme gerektirir. +"Doğrulanamaz" "bulunamaz" anlamına gelmez. Bu 11 türün tamamı yine de tespit edilir ve çıktınızda görünür. Kimlik bilgisinin canlı olup olmadığını ve döndürülmesi gerekip gerekmediğini belirlemek için manuel inceleme gerektirir. ::: ## Kapsam özeti | Kategori | Sayı | |----------|------| -| Canlı doğrulanan | 48 | +| Canlı doğrulanan | 39 | +| Güvenilir/eşlik eden bağlam gerektiren | 9 | | Yalnızca format doğrulaması | 6 | -| Doğrulanamaz | 10 | -| **Toplam dedektör** | **64** | -| **Doğrulayıcı (herhangi bir kapsam)** | **54 (%84,4)** | +| Doğrulanamaz | 11 | +| **Toplam dedektör** | **65** | +| **Kayıtlı doğrulayıcı implementasyonu** | **54 (%83,1)** | ## Ayrıca bakın diff --git a/go.mod b/go.mod index 2c35beb..b2eeb02 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/cloudflare/ahocorasick v0.0.0-20240916140611-054963ec9396 github.com/go-git/go-git/v5 v5.19.1 github.com/google/go-containerregistry v0.21.3 + github.com/mattn/go-runewidth v0.0.27 github.com/slack-go/slack v0.20.0 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 @@ -22,6 +23,8 @@ require ( google.golang.org/api v0.289.0 ) +require github.com/clipperhouse/uax29/v2 v2.2.0 // indirect + require ( cel.dev/expr v0.25.1 // indirect cloud.google.com/go v0.123.0 // indirect @@ -82,7 +85,7 @@ require ( github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 github.com/pjbgf/sha1cd v0.6.0 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect @@ -116,9 +119,9 @@ require ( google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect - google.golang.org/grpc v1.82.0 // indirect + google.golang.org/grpc v1.82.1 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + gopkg.in/yaml.v3 v3.0.1 gotest.tools/v3 v3.5.2 // indirect ) diff --git a/go.sum b/go.sum index 75d69a2..2ab03da 100644 --- a/go.sum +++ b/go.sum @@ -77,6 +77,8 @@ github.com/aws/smithy-go v1.27.4 h1:JQcphmBN4f0q/sPqXqROIItRNV/hy10cgu7CsFy616M= github.com/aws/smithy-go v1.27.4/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY= +github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cloudflare/ahocorasick v0.0.0-20240916140611-054963ec9396 h1:W2HK1IdCnCGuLUeyizSCkwvBjdj0ZL7mxnJYQ3poyzI= github.com/cloudflare/ahocorasick v0.0.0-20240916140611-054963ec9396/go.mod h1:tGWUZLZp9ajsxUOnHmFFLnqnlKXsCn6GReG4jAD59H0= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= @@ -174,6 +176,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-runewidth v0.0.27 h1:Feg/Oou5zI/wnpgDF6omIU0OokC9GxLC/WRknhVlIR0= +github.com/mattn/go-runewidth v0.0.27/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= @@ -296,8 +300,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 h1: google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7/go.mod h1:KqHwBx2upmfa1XSi1WuRvC+2VGCLtooKkfmyvRbUmqA= google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 h1:qEHAMpSaUhtD0p3NbEEI83HwNGFxEwaSJ1G9PLnCBZE= google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU= -google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/config/config.go b/internal/config/config.go index a51fad4..284c885 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -9,17 +9,9 @@ import ( "github.com/spf13/viper" "github.com/HodeTech/leakwatch/internal/detector/custom" + "github.com/HodeTech/leakwatch/internal/meta" ) -// Supported output formats. -var validFormats = map[string]bool{ - "json": true, - "sarif": true, - "csv": true, - "table": true, - "github": true, -} - // Supported severity levels for output.severity-threshold. var validSeverities = map[string]bool{ "low": true, @@ -141,7 +133,7 @@ func (c *Config) validate() error { if c.Scan.MaxFileSize > maxFileSizeCeiling { return fmt.Errorf("invalid max-file-size value: %d exceeds maximum of %d bytes (%dMB); per-file scanning is fully in-memory, so raising this limit risks large per-job memory usage", c.Scan.MaxFileSize, maxFileSizeCeiling, maxFileSizeCeiling/(1024*1024)) } - if !validFormats[c.Output.Format] { + if !meta.IsOutputFormat(c.Output.Format) { return fmt.Errorf("unsupported output format: %s", c.Output.Format) } if c.Detection.Entropy.Threshold < 0 || c.Detection.Entropy.Threshold > 8.0 { diff --git a/internal/detector/auth0/auth0_token.go b/internal/detector/auth0/auth0_token.go index 310278d..4482ccf 100644 --- a/internal/detector/auth0/auth0_token.go +++ b/internal/detector/auth0/auth0_token.go @@ -2,6 +2,7 @@ package auth0 import ( + "bytes" "context" "regexp" @@ -9,7 +10,7 @@ import ( "github.com/HodeTech/leakwatch/pkg/finding" ) -var auth0TokenPattern = regexp.MustCompile(`(?:AUTH0_MANAGEMENT_TOKEN|AUTH0_API_TOKEN|auth0_token)\s*[=:]\s*['"]?([A-Za-z0-9_-]{30,})['"]?`) +var auth0TokenPattern = regexp.MustCompile(`(?:["']?(?:AUTH0_MANAGEMENT_TOKEN|AUTH0_API_TOKEN|auth0_token)["']?)[ \t]*[=:][ \t]*["']?([A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{8,})`) // Detector detects Auth0 Management API Tokens. type Detector struct{} @@ -31,26 +32,40 @@ func (d *Detector) Severity() finding.Severity { return finding.SeverityCritical // Scan searches the data for Auth0 Management API Token patterns. // The token value is extracted from submatch group 1 and redacted to first 8 chars + ****. func (d *Detector) Scan(_ context.Context, data []byte) []detector.RawFinding { - allMatches := auth0TokenPattern.FindAllSubmatch(data, -1) + allMatches := auth0TokenPattern.FindAllSubmatchIndex(data, -1) if len(allMatches) == 0 { return nil } findings := make([]detector.RawFinding, 0, len(allMatches)) for _, groups := range allMatches { - fullMatch := groups[0] - tokenValue := groups[1] + if len(groups) < 4 || groups[2] < 0 || !auth0TokenBoundary(data, groups[3]) { + continue + } + fullMatch := bytes.Clone(data[groups[0]:groups[1]]) + tokenValue := bytes.Clone(data[groups[2]:groups[3]]) findings = append(findings, detector.RawFinding{ DetectorID: d.ID(), Raw: tokenValue, RawV2: fullMatch, Redacted: detector.RedactBytes(tokenValue), + ByteStart: groups[2], + ByteEnd: groups[3], }) } return findings } +func auth0TokenBoundary(data []byte, end int) bool { + if end >= len(data) { + return true + } + next := data[end] + return (next < 'A' || next > 'Z') && (next < 'a' || next > 'z') && + (next < '0' || next > '9') && next != '_' && next != '-' && next != '.' +} + func init() { detector.Register(&Detector{}) } diff --git a/internal/detector/auth0/auth0_token_test.go b/internal/detector/auth0/auth0_token_test.go index 312e453..87cbbd7 100644 --- a/internal/detector/auth0/auth0_token_test.go +++ b/internal/detector/auth0/auth0_token_test.go @@ -18,8 +18,7 @@ func TestDetector_Metadata_ReturnsExpectedValues(t *testing.T) { } func TestDetector_Scan_MatchesValidTokens(t *testing.T) { - // Synthetic 40-character token value. - token40 := "eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp3VDIifQ" + token := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2ZpeHR1cmUuZXUuYXV0aDAuY29tLyJ9.c2lnbmF0dXJlLWZpeHR1cmU" tests := []struct { name string @@ -30,52 +29,52 @@ func TestDetector_Scan_MatchesValidTokens(t *testing.T) { }{ { name: "AUTH0_MANAGEMENT_TOKEN with equals", - input: "AUTH0_MANAGEMENT_TOKEN=" + token40, + input: "AUTH0_MANAGEMENT_TOKEN=" + token, expected: 1, - redacted: "****" + token40[len(token40)-4:], - rawLen: len(token40), + redacted: "****" + token[len(token)-4:], + rawLen: len(token), }, { name: "AUTH0_API_TOKEN with equals", - input: "AUTH0_API_TOKEN=" + token40, + input: "AUTH0_API_TOKEN=" + token, expected: 1, - redacted: "****" + token40[len(token40)-4:], - rawLen: len(token40), + redacted: "****" + token[len(token)-4:], + rawLen: len(token), }, { name: "auth0_token lowercase with equals", - input: "auth0_token=" + token40, + input: "auth0_token=" + token, expected: 1, - redacted: "****" + token40[len(token40)-4:], - rawLen: len(token40), + redacted: "****" + token[len(token)-4:], + rawLen: len(token), }, { name: "AUTH0_MANAGEMENT_TOKEN with colon separator", - input: "AUTH0_MANAGEMENT_TOKEN: " + token40, + input: "AUTH0_MANAGEMENT_TOKEN: " + token, expected: 1, - redacted: "****" + token40[len(token40)-4:], - rawLen: len(token40), + redacted: "****" + token[len(token)-4:], + rawLen: len(token), }, { name: "AUTH0_API_TOKEN with single quotes", - input: "AUTH0_API_TOKEN='" + token40 + "'", + input: "AUTH0_API_TOKEN='" + token + "'", expected: 1, - redacted: "****" + token40[len(token40)-4:], - rawLen: len(token40), + redacted: "****" + token[len(token)-4:], + rawLen: len(token), }, { name: "AUTH0_MANAGEMENT_TOKEN with double quotes", - input: `AUTH0_MANAGEMENT_TOKEN="` + token40 + `"`, + input: `"AUTH0_MANAGEMENT_TOKEN": "` + token + `"`, expected: 1, - redacted: "****" + token40[len(token40)-4:], - rawLen: len(token40), + redacted: "****" + token[len(token)-4:], + rawLen: len(token), }, { name: "token with spaces around equals", - input: "AUTH0_MANAGEMENT_TOKEN = " + token40, + input: "AUTH0_MANAGEMENT_TOKEN = " + token, expected: 1, - redacted: "****" + token40[len(token40)-4:], - rawLen: len(token40), + redacted: "****" + token[len(token)-4:], + rawLen: len(token), }, } @@ -88,6 +87,8 @@ func TestDetector_Scan_MatchesValidTokens(t *testing.T) { require.NotEmpty(t, findings) assert.Equal(t, tt.redacted, findings[0].Redacted) assert.Len(t, findings[0].Raw, tt.rawLen) + assert.Equal(t, token, string(findings[0].Raw)) + assert.Equal(t, token, tt.input[findings[0].ByteStart:findings[0].ByteEnd]) } }) } @@ -102,9 +103,17 @@ func TestDetector_Scan_RejectsInvalidInput(t *testing.T) { name: "too short token value", input: "AUTH0_MANAGEMENT_TOKEN=abc123", }, + { + name: "opaque value is not a management JWT", + input: "AUTH0_MANAGEMENT_TOKEN=eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp3VDIifQ", + }, + { + name: "fourth JWT segment", + input: "AUTH0_MANAGEMENT_TOKEN=aaaaaaaaaaaaaaaa.bbbbbbbbbbbbbbbb.cccccccc.more", + }, { name: "no recognized variable name", - input: "API_TOKEN=eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp3VDIifQ", + input: "API_TOKEN=aaaaaaaaaaaaaaaa.bbbbbbbbbbbbbbbb.cccccccc", }, { name: "plain text", diff --git a/internal/detector/custom/custom_rule.go b/internal/detector/custom/custom_rule.go index ffdb2e5..fa0be8c 100644 --- a/internal/detector/custom/custom_rule.go +++ b/internal/detector/custom/custom_rule.go @@ -108,34 +108,6 @@ func (d *CustomDetector) Scan(_ context.Context, data []byte) []detector.RawFind return findings } -// RegisterCustomRules parses RuleDefs, creates detectors, and registers them. -// Returns the count of successfully registered rules and any errors. -// -// A rule whose ID collides with an already-registered detector (a built-in -// detector or a previously registered custom rule) is skipped with an error -// rather than registered, because detector.Register panics on duplicate IDs. -func RegisterCustomRules(rules []RuleDef) (int, []error) { - var errs []error - count := 0 - - for _, def := range rules { - det, err := NewFromDef(def) - if err != nil { - errs = append(errs, err) - continue - } - // RegisterIfAbsent checks-and-inserts atomically, so a colliding ID is - // rejected without the panic that detector.Register would raise. - if !detector.RegisterIfAbsent(det) { - errs = append(errs, fmt.Errorf("custom rule %q: ID already registered (built-in detector or duplicate custom rule)", det.ID())) - continue - } - count++ - } - - return count, errs -} - // parseSeverity converts a rule's YAML severity string into a finding.Severity. // The second return value is false when s is a non-empty string that does not // match one of the four recognized (lowercase, exact) severity names; callers diff --git a/internal/detector/custom/custom_rule_test.go b/internal/detector/custom/custom_rule_test.go index 7345296..48e7e5b 100644 --- a/internal/detector/custom/custom_rule_test.go +++ b/internal/detector/custom/custom_rule_test.go @@ -5,7 +5,6 @@ import ( "strings" "testing" - "github.com/HodeTech/leakwatch/internal/detector" "github.com/HodeTech/leakwatch/pkg/finding" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -200,58 +199,3 @@ func TestParseSeverity_TableDriven(t *testing.T) { }) } } - -func TestRegisterCustomRules_ValidRules_RegistersAll(t *testing.T) { - detector.Reset() - defer detector.Reset() - - rules := []RuleDef{ - {ID: "custom-1", Regex: `CUSTOM1_[A-Z]{10}`, Keywords: []string{"CUSTOM1_"}}, - {ID: "custom-2", Regex: `CUSTOM2_[A-Z]{10}`, Keywords: []string{"CUSTOM2_"}}, - } - - count, errs := RegisterCustomRules(rules) - assert.Equal(t, 2, count) - assert.Empty(t, errs) - - all := detector.All() - assert.Len(t, all, 2) -} - -func TestRegisterCustomRules_MixedValidity_RegistersValidOnly(t *testing.T) { - detector.Reset() - defer detector.Reset() - - rules := []RuleDef{ - {ID: "valid", Regex: `VALID_[A-Z]{10}`}, - {ID: "invalid", Regex: `[unclosed`}, - } - - count, errs := RegisterCustomRules(rules) - assert.Equal(t, 1, count) - assert.Len(t, errs, 1) -} - -func TestRegisterCustomRules_DuplicateID_SkipsWithoutPanic(t *testing.T) { - detector.Reset() - defer detector.Reset() - - // A custom rule whose ID collides with an already-registered detector must - // be skipped with an error — never registered, because detector.Register - // panics on duplicate IDs. - first := []RuleDef{{ID: "dupe", Regex: `DUPE_[A-Z]{10}`}} - count, errs := RegisterCustomRules(first) - require.Equal(t, 1, count) - require.Empty(t, errs) - - assert.NotPanics(t, func() { - second := []RuleDef{{ID: "dupe", Regex: `OTHER_[A-Z]{10}`}} - count, errs = RegisterCustomRules(second) - }) - assert.Equal(t, 0, count) - require.Len(t, errs, 1) - assert.Contains(t, errs[0].Error(), "already registered") - - // Only the original detector remains registered. - assert.Len(t, detector.All(), 1) -} diff --git a/internal/detector/detector.go b/internal/detector/detector.go index 5431132..499f261 100644 --- a/internal/detector/detector.go +++ b/internal/detector/detector.go @@ -3,6 +3,7 @@ package detector import ( "context" + "regexp" "github.com/HodeTech/leakwatch/pkg/finding" ) @@ -19,13 +20,64 @@ type Detector interface { // If empty, pre-filtering is skipped and regex is applied to every chunk. Keywords() []string - // Scan scans the given data and returns potential secret findings. + // Scan scans the given data and returns potential secret findings. Results + // MUST be emitted in stable, left-to-right source order. When ByteStart and + // ByteEnd are present they must be monotonically non-decreasing by ByteStart; + // legacy findings without spans must preserve their matcher's source order. + // The engine uses this contract to disambiguate repeated identical values. Scan(ctx context.Context, data []byte) []RawFinding // Severity returns the default severity for findings from this detector. Severity() finding.Severity } +// OverlapFallback is an optional detector contract for broad contextual +// detectors. When an authoritative provider detector reports an intersecting +// byte range in the same source chunk at equal or greater severity, the engine +// suppresses the fallback finding and preserves the provider-specific +// verification and remediation. Implementations must return findings in a +// bounded set because the engine briefly retains fallback candidates until all +// authoritative detectors have scanned that chunk. +type OverlapFallback interface { + Detector + FallbackOnSpecializedOverlap() bool +} + +// OverlapAuthority is an optional detector contract for built-in, +// provider-specific detectors that may replace a broad OverlapFallback finding +// at the same source bytes. Custom and heuristic detectors must not implement +// this contract: overlap alone does not make a finding more authoritative. +type OverlapAuthority interface { + Detector + AuthoritativeOnOverlap() bool +} + +// PlaygroundPatternContract describes detector correlation that the static +// browser playground must preserve. Most detectors are represented by a set +// of independent trigger patterns and do not implement this contract. A +// detector whose finding requires nearby companion context must expose the +// primary and companion regexes here so the site generator does not flatten +// an AND relationship into false-positive OR triggers. +// +// This is an optional presentation contract only. The Go detector remains the +// source of truth for findings and must enforce the same (or stricter) rules in +// Scan. +type PlaygroundPatternContract struct { + Primary []*regexp.Regexp + RequiredNearby []*regexp.Regexp + ProximityBytes int + SameLogicalBlock bool + RejectPlaceholders bool + OneToOne bool +} + +// PlaygroundCorrelated is implemented by detectors whose browser-preview +// representation requires companion correlation. +type PlaygroundCorrelated interface { + Detector + PlaygroundPatternContract() PlaygroundPatternContract +} + // RawFinding represents an unverified raw finding. type RawFinding struct { DetectorID string @@ -33,4 +85,11 @@ type RawFinding struct { RawV2 []byte Redacted string ExtraData map[string]string + // ByteStart and ByteEnd identify Raw's exact half-open byte span in the + // scanned chunk. A zero/zero span means "not provided" for backward + // compatibility. The engine validates both bounds and bytes before trusting + // the span, then falls back to legacy raw-value searching when absent or + // invalid. + ByteStart int + ByteEnd int } diff --git a/internal/detector/generic/generic_api_key.go b/internal/detector/generic/generic_api_key.go index 5a8fbe2..5c27434 100644 --- a/internal/detector/generic/generic_api_key.go +++ b/internal/detector/generic/generic_api_key.go @@ -5,6 +5,7 @@ import ( "bytes" "context" "regexp" + "strings" "github.com/HodeTech/leakwatch/internal/detector" "github.com/HodeTech/leakwatch/internal/entropy" @@ -18,7 +19,18 @@ import ( // vendor-specific secret in the codebase gets its own package, so a future // cleanup extracting `apisix` into its own detector would be consistent with // that convention, but is not required by this change. -var apiKeyPattern = regexp.MustCompile(`(?i)(api[_\-]?key|api[_\-]?secret|secret[_\-]?key|x[_\-]?apisix[_\-]?key|apisix[_\-]?key|apisix[_\-]?admin[_\-]?key)[ \t]*[:=][ \t]*['"]?([a-zA-Z0-9/+=\-_]{16,64})['"]?`) +// apiKeyPattern accepts shell/YAML assignments and quoted object keys while +// capturing the character before the key and the closing key quote separately. +// This prevents an enclosing shell-string quote from being mistaken for a key +// quote while still validating JSON/TOML quoted keys. Scan also validates that +// the value ends at a real assignment +// boundary; keeping those checks in Go avoids unsupported regexp +// backreferences and prevents prefix matches on malformed or overlong values. +// +// Capture groups: key-prefix, key, key-close, value-open, value, value-close. +var apiKeyPattern = regexp.MustCompile(`(?i)(^|[^a-zA-Z0-9_])(x[_\-]?apisix[_\-]?key|x[_\-]?api[_\-]?key|apisix[_\-]?admin[_\-]?key|apisix[_\-]?key|api[_\-]?key|api[_\-]?secret|secret[_\-]?key)(['"]?)[ \t\r\n]*[:=][ \t\r\n]*(['"]?)([a-zA-Z0-9/+=\-_]{16,64})(['"]?)`) + +var apiKeyNameReplacer = strings.NewReplacer("-", "", "_", "") // APIKeyDetector detects generic API key assignments. type APIKeyDetector struct{} @@ -31,7 +43,11 @@ func (d *APIKeyDetector) Keywords() []string { "api_key", "api-key", "apikey", "api_secret", "api-secret", "apisecret", "secret_key", "secret-key", "secretkey", - "apisix-key", "apisix_key", "x-apisix-key", "x_apisix_key", "apisix-admin-key", + // The common "apisix" stem aligns every separator variation accepted by + // the regexp (for example APISIX_ADMIN_KEY and APISIXADMINKEY) with the + // matcher. Official X-API-KEY spellings are already covered by the + // api-key/api_key/apikey substrings above. + "apisix", } } func (d *APIKeyDetector) Severity() finding.Severity { return finding.SeverityMedium } @@ -43,6 +59,15 @@ func (d *APIKeyDetector) Severity() finding.Severity { return finding.SeverityMe // never entropy-gated. func (d *APIKeyDetector) EntropyBased() bool { return true } +// EntropyGated reports whether one raw finding should be subject to the +// engine-level entropy threshold. Explicit APISIX Admin API header names are a +// strong structural context and are therefore not entropy-gated; real APISIX +// keys are commonly 32-character hex values whose Shannon entropy can be below +// the generic threshold. Other generic assignments retain both entropy gates. +func (d *APIKeyDetector) EntropyGated(raw detector.RawFinding) bool { + return raw.ExtraData["key_context"] != "apisix" +} + // minEntropy is the Shannon entropy floor a candidate value must clear to be // considered plausibly random secret material rather than an ordinary // identifier or human-readable placeholder. Raised from the original 3.0, @@ -69,47 +94,178 @@ const highVowelRatioThreshold = 0.35 // natural-language text rather than random secret material, are skipped as // unlikely to be real secrets. func (d *APIKeyDetector) Scan(_ context.Context, data []byte) []detector.RawFinding { - matches := apiKeyPattern.FindAllSubmatch(data, -1) + matches := apiKeyPattern.FindAllSubmatchIndex(data, -1) if len(matches) == 0 { return nil } + apisixPositions := asciiFoldPositions(data, []byte("apisix")) findings := make([]detector.RawFinding, 0, len(matches)) for _, match := range matches { - if len(match) < 3 { + keyPrefix := submatchBytes(data, match, 1) + key := submatchBytes(data, match, 2) + keyClosingQuote := submatchBytes(data, match, 3) + valueOpen := submatchBytes(data, match, 4) + value := submatchBytes(data, match, 5) + valueClosingQuote := submatchBytes(data, match, 6) + if !validKeyQuoteContext(keyPrefix, keyClosingQuote) || + !validValueQuoteContext(keyPrefix, keyClosingQuote, valueOpen, valueClosingQuote) { + continue + } + if len(valueOpen) == 0 && len(valueClosingQuote) == 0 && !hasAssignmentBoundary(data, match[1]) { + continue + } + + // Reject cheap, deterministic false positives before provider-context and + // entropy work. The package-level replacer and one APISIX position scan per + // chunk keep the detector O(file size + matches). + if isPlaceholder(value) || isDegenerateValue(value) || isBareReference(value) { continue } - value := match[2] + strongContext := hasStrongAPISIXContext(string(key), match[10], match[11], apisixPositions) // Skip low-entropy values — unlikely to be real secrets - if entropy.Calculate(value) < minEntropy { + if !strongContext && entropy.Calculate(value) < minEntropy { continue } // Skip values whose letter composition reads as natural-language text // (env var names, human-readable placeholders) rather than random // secret material. - if hasHighVowelRatio(value) { + if !strongContext && hasHighVowelRatio(value) { continue } - // Skip placeholder/example values - if isPlaceholder(value) { - continue + extraData := map[string]string{"key_name": string(key)} + if strongContext { + extraData["key_context"] = "apisix" } findings = append(findings, detector.RawFinding{ DetectorID: d.ID(), Raw: value, Redacted: detector.RedactBytes(value), - ExtraData: map[string]string{ - "key_name": string(match[1]), - }, + ExtraData: extraData, + ByteStart: match[10], + ByteEnd: match[11], }) } return findings } +func submatchBytes(data []byte, indexes []int, group int) []byte { + start := indexes[group*2] + end := indexes[group*2+1] + return data[start:end] +} + +func validQuotePair(open, closingQuote []byte) bool { + if len(open) == 0 || len(closingQuote) == 0 { + return len(open) == 0 && len(closingQuote) == 0 + } + return len(open) == 1 && len(closingQuote) == 1 && open[0] == closingQuote[0] +} + +func validKeyQuoteContext(prefix, closingQuote []byte) bool { + if len(closingQuote) == 0 { + return true + } + return len(prefix) == 1 && len(closingQuote) == 1 && + (prefix[0] == '\'' || prefix[0] == '"') && prefix[0] == closingQuote[0] +} + +func validValueQuoteContext(prefix, keyClosingQuote, open, closingQuote []byte) bool { + if validQuotePair(open, closingQuote) { + return true + } + // In `curl -H "x-api-key: value"`, the surrounding shell quote appears + // before the key and after the value. It is not a value quote pair. + return len(keyClosingQuote) == 0 && len(open) == 0 && len(closingQuote) == 1 && + len(prefix) == 1 && prefix[0] == closingQuote[0] && + (prefix[0] == '\'' || prefix[0] == '"') +} + +// hasAssignmentBoundary rejects partial captures such as a 64-byte prefix of +// a longer value or the prefix before a disallowed character. Quoted values +// have already consumed their closing quote; both quoted and unquoted forms +// must then end at a configuration-token boundary. +func hasAssignmentBoundary(data []byte, end int) bool { + if end >= len(data) { + return true + } + return !isAPIKeyValueContinuation(data[end]) +} + +func isAPIKeyValueContinuation(value byte) bool { + return value >= 'a' && value <= 'z' || value >= 'A' && value <= 'Z' || + value >= '0' && value <= '9' || strings.ContainsRune("/+=-_.", rune(value)) +} + +func hasStrongAPISIXContext(key string, valueStart, valueEnd int, apisixPositions []int) bool { + canonical := apiKeyNameReplacer.Replace(strings.ToLower(key)) + switch canonical { + case "xapisixkey", "apisixkey", "apisixadminkey": + return true + case "xapikey": + // Context must be independent of the candidate value itself. Otherwise a + // generic X-API-KEY whose value happens to contain "apisix" could disable + // its own false-positive gates. + return apisixOutsideValue(apisixPositions, valueStart, valueEnd) + default: + return false + } +} + +func asciiFoldPositions(data, needle []byte) []int { + var positions []int + for i := 0; i+len(needle) <= len(data); i++ { + if bytes.EqualFold(data[i:i+len(needle)], needle) { + positions = append(positions, i) + } + } + return positions +} + +func apisixOutsideValue(positions []int, valueStart, valueEnd int) bool { + if len(positions) == 0 { + return false + } + return positions[0]+len("apisix") <= valueStart || positions[len(positions)-1] >= valueEnd +} + +func isDegenerateValue(value []byte) bool { + if len(value) == 0 { + return true + } + for _, b := range value[1:] { + if b != value[0] { + return false + } + } + return true +} + +// isBareReference rejects identifier-like indirections such as +// APISIX_ADMIN_KEY. Braced environment/secret-manager references are already +// outside the candidate character class; this covers the common unbraced form +// without suppressing mixed-case or punctuation-rich real credentials. +func isBareReference(value []byte) bool { + hasSeparator := false + hasLetter := false + for _, b := range value { + switch { + case b >= 'A' && b <= 'Z': + hasLetter = true + case b >= '0' && b <= '9': + case b == '_': + hasSeparator = true + default: + return false + } + } + return hasLetter && hasSeparator +} + // placeholderPatterns are common dummy values used in example configs. Every // entry MUST be lowercase: isPlaceholder compares against a lowercased copy // of the candidate value, so a mixed-case entry here would never match diff --git a/internal/detector/generic/generic_api_key_test.go b/internal/detector/generic/generic_api_key_test.go index 7c7c00e..d28e3d0 100644 --- a/internal/detector/generic/generic_api_key_test.go +++ b/internal/detector/generic/generic_api_key_test.go @@ -1,10 +1,17 @@ package generic import ( + "bytes" "context" + "os" + "path/filepath" "testing" + "time" + "github.com/HodeTech/leakwatch/internal/detector" "github.com/HodeTech/leakwatch/internal/detector/testutil" + "github.com/HodeTech/leakwatch/internal/engine" + "github.com/HodeTech/leakwatch/internal/source" "github.com/HodeTech/leakwatch/pkg/finding" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -79,6 +86,111 @@ func TestAPIKeyDetector_Scan(t *testing.T) { input: `api_key = "dGhpcyBpcyBhIHRlc3Qga2V5IGZvciBsZWFrd2F0Y2g="`, expected: 1, }, + { + name: "SDK constructor quoted value", + input: `client = OpenAI(api_key="Q7mN2pL9rT4vW8xYzC6bH3kF")`, + expected: 1, + }, + { + name: "URL query unquoted value", + input: `https://api.example.test/?api_key=Q7mN2pL9rT4vW8xYzC6bH3kF&next=1`, + expected: 1, + }, + { + name: "curl double quoted header", + input: `curl -H "x-api-key: Q7mN2pL9rT4vW8xYzC6bH3kF" https://api.example.test`, + expected: 1, + }, + { + name: "curl single quoted header", + input: `curl -H 'X-API-Key: Q7mN2pL9rT4vW8xYzC6bH3kF' https://api.example.test`, + expected: 1, + }, + { + name: "embedded shell command", + input: `run: "curl -H \"x-api-key: Q7mN2pL9rT4vW8xYzC6bH3kF\" https://api.example.test"`, + expected: 1, + }, + { + name: "XML attribute quoted value", + input: ``, + expected: 1, + }, + { + name: "double quoted JSON key", + input: `{"X-APISIX-KEY": "Q7mN2pL9rT4vW8xY0zA1bC3dE5fG6hJ8kM2nP4sR7tV"}`, + expected: 1, + }, + { + name: "single quoted object key", + input: `{'x-apisix-key' : 'Q7mN2pL9rT4vW8xY0zA1bC3dE5fG6hJ8kM2nP4sR7tV'}`, + expected: 1, + }, + { + name: "official APISIX header with independent context and low entropy hex value", + input: `{"Gateway": "Apache APISIX", "X-API-KEY": "01234567012345670123456701234567"}`, + expected: 1, + }, + { + name: "generic X-API-KEY without APISIX context retains entropy filters", + input: `{"X-API-KEY": "01234567012345670123456701234567"}`, + expected: 0, + }, + { + name: "generic X-API-KEY natural language value is not structural", + input: `{"X-API-KEY": "development_environment"}`, + expected: 0, + }, + { + name: "multiline JSON whitespace", + input: "{\r\n \"Gateway\": \"APISIX\",\r\n \"X-API-KEY\"\r\n :\r\n \"01234567012345670123456701234567\"\r\n}", + expected: 1, + }, + { + name: "sensitive header list entry is not an assignment", + input: `{"SensitiveHeaders": ["Authorization", "X-APISIX-KEY"]}`, + expected: 0, + }, + { + name: "mismatched key quotes", + input: `{"X-APISIX-KEY': "Q7mN2pL9rT4vW8xY0zA1bC3dE5fG6hJ8kM2nP4sR7tV"}`, + expected: 0, + }, + { + name: "mismatched value quotes", + input: `{"X-APISIX-KEY": "Q7mN2pL9rT4vW8xY0zA1bC3dE5fG6hJ8kM2nP4sR7tV'}`, + expected: 0, + }, + { + name: "unterminated quoted value", + input: `{"X-APISIX-KEY": "Q7mN2pL9rT4vW8xY0zA1bC3dE5fG6hJ8kM2nP4sR7tV}`, + expected: 0, + }, + { + name: "disallowed value suffix", + input: `{"X-APISIX-KEY": "Q7mN2pL9rT4vW8xY.invalid"}`, + expected: 0, + }, + { + name: "unquoted disallowed value suffix", + input: `X-APISIX-KEY = Q7mN2pL9rT4vW8xY.invalid`, + expected: 0, + }, + { + name: "overlong allowed value is rejected not truncated", + input: `{"X-APISIX-KEY": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0"}`, + expected: 0, + }, + { + name: "degenerate explicit APISIX value", + input: `{"X-APISIX-KEY": "0000000000000000"}`, + expected: 0, + }, + { + name: "bare environment reference in explicit APISIX field", + input: `{"X-APISIX-KEY": "APISIX_ADMIN_KEY"}`, + expected: 0, + }, } d := &APIKeyDetector{} @@ -270,9 +382,165 @@ func TestAPIKeyDetector_ScanViaMatcher_KeywordRegexAlignment(t *testing.T) { assert.Equal(t, "generic-api-key", findings[0].DetectorID) } +type fixtureSource struct { + data []byte +} + +func (s *fixtureSource) Type() string { return "fixture" } +func (s *fixtureSource) Validate(context.Context) error { return nil } +func (s *fixtureSource) Err() error { return nil } +func (s *fixtureSource) Chunks(_ context.Context) <-chan source.Chunk { + chunks := make(chan source.Chunk, 1) + chunks <- source.Chunk{ + Data: s.data, + SourceMetadata: finding.SourceMetadata{ + SourceType: "filesystem", + FilePath: "appsettings.synthetic.json", + }, + } + close(chunks) + return chunks +} + +func readSyntheticAppsettings(t *testing.T) []byte { + t.Helper() + data, err := os.ReadFile(filepath.Join("testdata", "appsettings.synthetic.json")) + require.NoError(t, err) + return data +} + +func TestAPIKeyDetector_AppsettingsJSON_DirectAndMatcherParity(t *testing.T) { + data := readSyntheticAppsettings(t) + d := &APIKeyDetector{} + + direct := d.Scan(context.Background(), data) + viaMatcher := testutil.ScanViaMatcher(d, data) + + require.Len(t, direct, 3, "the three APISIX assignments must be detected") + require.Equal(t, direct, viaMatcher, "quoted JSON assignments must survive the production matcher gate") + for _, got := range direct { + assert.Equal(t, "X-APISIX-KEY", got.ExtraData["key_name"]) + assert.NotEqual(t, string(got.Raw), got.Redacted) + } +} + +func TestAPIKeyDetector_APISIXSpellings_IsolatedMatcherParity(t *testing.T) { + tests := []struct { + name string + key string + }{ + {name: "official header hyphen", key: "X-API-KEY"}, + {name: "official header underscore", key: "X_API_KEY"}, + {name: "local header", key: "X-APISIX-KEY"}, + {name: "admin key underscore", key: "APISIX_ADMIN_KEY"}, + {name: "admin key compact", key: "APISIXADMINKEY"}, + {name: "apisix key mixed separators", key: "APISIX-KEY"}, + } + + d := &APIKeyDetector{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + input := []byte(`{"Gateway": "Apache APISIX", "` + tt.key + `": "01234567012345670123456701234567"}`) + direct := d.Scan(context.Background(), input) + viaMatcher := testutil.ScanViaMatcher(d, input) + require.Len(t, direct, 1) + require.Equal(t, direct, viaMatcher, "each accepted spelling must independently select the detector") + assert.Equal(t, tt.key, direct[0].ExtraData["key_name"]) + assert.Equal(t, "apisix", direct[0].ExtraData["key_context"]) + assert.False(t, d.EntropyGated(direct[0]), "explicit APISIX context must bypass entropy gating") + assert.Equal(t, string(direct[0].Raw), string(input[direct[0].ByteStart:direct[0].ByteEnd])) + }) + } +} + +func TestAPIKeyDetector_XAPIKey_ValueCannotProvideItsOwnAPISIXContext(t *testing.T) { + data := []byte(`{"X-API-KEY": "aB3kAPISIX9mN2pQ7rT4vW8xY0zC5dF"}`) + d := &APIKeyDetector{} + findings := d.Scan(context.Background(), data) + require.Len(t, findings, 1, "high-entropy generic header remains detectable") + assert.Empty(t, findings[0].ExtraData["key_context"]) + assert.True(t, d.EntropyGated(findings[0]), "candidate bytes must not establish their own structural context") +} + +func TestAPIKeyDetector_AppsettingsJSON_FullEngine(t *testing.T) { + data := readSyntheticAppsettings(t) + d := &APIKeyDetector{} + eng := engine.New(engine.Config{ + Concurrency: 1, + Detectors: []detector.Detector{d}, + EnableEntropy: true, + EntropyThreshold: 4.0, + Clock: func() time.Time { + return time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) + }, + }) + + result, err := eng.Scan(context.Background(), &fixtureSource{data: data}) + require.NoError(t, err) + require.Len(t, result.Findings, 3, "all APISIX assignments must survive matcher, detector, and entropy gates") + + lines := make([]int, 0, len(result.Findings)) + for _, got := range result.Findings { + assert.Equal(t, "generic-api-key", got.DetectorID) + assert.Empty(t, got.Raw, "raw values must not be exposed by the default engine result") + lines = append(lines, got.SourceMetadata.Line) + } + assert.Equal(t, linesContaining(data, []byte(`"X-APISIX-KEY":`)), lines) +} + +func TestAPIKeyDetector_APISIXLowEntropy_SurvivesFullEngine(t *testing.T) { + data := []byte("{\n \"Gateway\": \"Apache APISIX\",\n \"X-API-KEY\": \"01234567012345670123456701234567\"\n}\n") + d := &APIKeyDetector{} + eng := engine.New(engine.Config{ + Concurrency: 1, + Detectors: []detector.Detector{d}, + EnableEntropy: true, + EntropyThreshold: 4.0, + }) + + result, err := eng.Scan(context.Background(), &fixtureSource{data: data}) + require.NoError(t, err) + require.Len(t, result.Findings, 1, "strong APISIX context must survive the generic entropy thresholds") + assert.Less(t, result.Findings[0].Entropy, 3.8, "fixture must prove the low-entropy structural path") + assert.Equal(t, 3, result.Findings[0].SourceMetadata.Line) +} + +func TestAPIKeyDetector_ExactSpan_PreventsDecoyLineAndIgnore(t *testing.T) { + value := "01234567012345670123456701234567" + data := []byte("{\n" + + ` "DocumentationExample": "` + value + `", // leakwatch:ignore:generic-api-key` + "\n" + + ` "X-APISIX-KEY": "` + value + `"` + "\n}\n") + d := &APIKeyDetector{} + eng := engine.New(engine.Config{ + Concurrency: 1, + Detectors: []detector.Detector{d}, + EnableEntropy: true, + EntropyThreshold: 4.0, + }) + + result, err := eng.Scan(context.Background(), &fixtureSource{data: data}) + require.NoError(t, err) + require.Len(t, result.Findings, 1, "an ignored same-value decoy must not suppress the actual assignment") + assert.Equal(t, 3, result.Findings[0].SourceMetadata.Line) +} + +func linesContaining(data, marker []byte) []int { + var lines []int + for i, line := range bytes.Split(data, []byte{'\n'}) { + if bytes.Contains(line, marker) { + lines = append(lines, i+1) + } + } + return lines +} + func TestAPIKeyDetector_EntropyBased(t *testing.T) { // The generic detector is heuristic (matches arbitrary high-entropy // strings), so it must opt into the engine's entropy floor. assert.True(t, (&APIKeyDetector{}).EntropyBased(), "generic-api-key must be entropy-based so the engine gates it on entropy") + + d := &APIKeyDetector{} + heuristic := detector.RawFinding{ExtraData: map[string]string{"key_name": "api_key"}} + assert.True(t, d.EntropyGated(heuristic), "ordinary generic assignments must retain engine entropy gating") } diff --git a/internal/detector/generic/structured_config.go b/internal/detector/generic/structured_config.go new file mode 100644 index 0000000..480315e --- /dev/null +++ b/internal/detector/generic/structured_config.go @@ -0,0 +1,815 @@ +package generic + +import ( + "bytes" + "context" + "encoding/json" + "strconv" + "strings" + "unicode" + "unicode/utf8" + + "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/internal/entropy" + "github.com/HodeTech/leakwatch/pkg/finding" +) + +const ( + maxStructuredTokens = 200_000 + maxStructuredDepth = 64 + maxStructuredStringLen = 256 * 1024 + maxStructuredFindings = 1_024 + minContextSecretLength = 4 +) + +// StructuredConfigDetector detects secrets stored in high-confidence leaf +// fields of JSON/JSONC, YAML, TOML, XML and dotenv configuration files. Each +// syntax has a bounded adapter; formats are not fed through one permissive +// cross-format regexp. Explicit field context can therefore detect human-style +// and short secrets without weakening the generic detector's false-positive +// gates. +type StructuredConfigDetector struct{} + +func (d *StructuredConfigDetector) ID() string { return "structured-config-secret" } +func (d *StructuredConfigDetector) Description() string { return "Structured Configuration Secret" } +func (d *StructuredConfigDetector) Severity() finding.Severity { + return finding.SeverityHigh +} + +func (d *StructuredConfigDetector) Keywords() []string { + // JSON permits escaped object keys (for example "Pass\u0077ord"), and the + // supported role vocabulary has many casing/separator forms. No finite raw + // substring list can preserve Scan/matcher parity for all of them. Running + // the bounded lexer for every text chunk is therefore a correctness choice, + // not an omitted optimization. + return nil +} + +// FallbackOnSpecializedOverlap marks this context detector as a fallback when a +// specialized detector identifies the same source value. The engine keeps the +// provider-specific severity, verification and remediation in that case. +func (d *StructuredConfigDetector) FallbackOnSpecializedOverlap() bool { return true } + +func (d *StructuredConfigDetector) Scan(ctx context.Context, data []byte) []detector.RawFinding { + scanData, baseOffset := stripUTF8BOM(data) + trimmed := bytes.TrimSpace(scanData) + if len(trimmed) == 0 { + return nil + } + var findings []detector.RawFinding + switch trimmed[0] { + case '{': + findings = d.scanJSON(ctx, scanData) + case '[': + firstLine := trimmed + if lineEnd := bytes.IndexByte(firstLine, '\n'); lineEnd >= 0 { + firstLine = firstLine[:lineEnd] + } + if _, tomlSection := parseTOMLSection(firstLine); tomlSection { + findings = d.scanLineConfig(ctx, scanData) + break + } + findings = d.scanJSON(ctx, scanData) + case '<': + findings = d.scanXML(ctx, scanData) + default: + findings = d.scanLineConfig(ctx, scanData) + } + if baseOffset != 0 { + for i := range findings { + findings[i].ByteStart += baseOffset + findings[i].ByteEnd += baseOffset + } + } + return findings +} + +func stripUTF8BOM(data []byte) ([]byte, int) { + if bytes.HasPrefix(data, []byte{0xef, 0xbb, 0xbf}) { + return data[3:], 3 + } + return data, 0 +} + +func (d *StructuredConfigDetector) scanJSON(ctx context.Context, data []byte) []detector.RawFinding { + tokens, candidates, complete := lexJSONLike(ctx, data) + if !complete || len(candidates) == 0 { + return nil + } + + paths := buildJSONPaths(tokens) + findings := make([]detector.RawFinding, 0, len(candidates)) + for _, candidate := range candidates { + keyToken, valueToken := candidate.key, candidate.value + raw := data[valueToken.contentStart:valueToken.contentEnd] + path := paths[valueToken.contentStart] + if path == "" { + path = keyToken.text + } + findings = append(findings, detector.RawFinding{ + DetectorID: d.ID(), + Raw: raw, + Redacted: detector.RedactBytes(raw), + ExtraData: map[string]string{ + "key_name": keyToken.text, + "key_path": path, + "config_format": "json", + }, + ByteStart: valueToken.contentStart, + ByteEnd: valueToken.contentEnd, + }) + } + return findings +} + +type jsonTokenKind uint8 + +const ( + jsonOther jsonTokenKind = iota + jsonString + jsonColon + jsonComma + jsonObjectStart + jsonObjectEnd + jsonArrayStart + jsonArrayEnd +) + +type jsonToken struct { + kind jsonTokenKind + start, end int + contentStart, contentEnd int + depth int + text string +} + +type structuredCandidate struct { + key jsonToken + value jsonToken +} + +// lexJSONLike tokenizes only the JSON structure needed for key/value +// extraction. It preserves byte offsets, skips // and /* */ comments, accepts +// trailing commas at the parser layer, and retains at most +// maxStructuredTokens tokens for path reconstruction. Candidate extraction +// continues through the entire bounded input even after that retention cap, so +// a large valid prefix cannot hide a secret near the end of a file. +func lexJSONLike(ctx context.Context, data []byte) ([]jsonToken, []structuredCandidate, bool) { + lexer := jsonLikeLexer{ + data: data, tokens: make([]jsonToken, 0, minInt(len(data)/8, 4_096)), + candidates: make([]structuredCandidate, 0, 8), + } + for position := 0; position < len(data); { + if ctx.Err() != nil { + return nil, nil, false + } + next, complete := lexer.scanToken(ctx, position) + if !complete { + return nil, nil, false + } + position = next + } + return lexer.tokens, lexer.candidates, true +} + +type jsonLikeLexer struct { + data []byte + tokens []jsonToken + candidates []structuredCandidate + previous [2]jsonToken + previousCount int + depth int +} + +func (l *jsonLikeLexer) scanToken(ctx context.Context, position int) (int, bool) { + value := l.data[position] + if value == ' ' || value == '\t' || value == '\r' || value == '\n' { + return position + 1, true + } + if value == '/' { + return l.scanCommentOrSlash(ctx, position) + } + if value == '"' { + token, next, ok, complete := scanJSONString(ctx, l.data, position, l.depth) + if ok { + l.emit(token) + } + return next, complete + } + if kind, opening, closing, ok := jsonStructuralToken(value); ok { + if closing && l.depth > 0 { + l.depth-- + } + l.emit(jsonToken{kind: kind, start: position, end: position + 1, depth: l.depth}) + if opening { + l.depth++ + } + return position + 1, true + } + return l.scanOtherToken(ctx, position) +} + +func jsonStructuralToken(value byte) (jsonTokenKind, bool, bool, bool) { + switch value { + case ':': + return jsonColon, false, false, true + case ',': + return jsonComma, false, false, true + case '{': + return jsonObjectStart, true, false, true + case '[': + return jsonArrayStart, true, false, true + case '}': + return jsonObjectEnd, false, true, true + case ']': + return jsonArrayEnd, false, true, true + default: + return jsonOther, false, false, false + } +} + +func (l *jsonLikeLexer) scanCommentOrSlash(ctx context.Context, position int) (int, bool) { + if position+1 >= len(l.data) || (l.data[position+1] != '/' && l.data[position+1] != '*') { + l.emit(jsonToken{kind: jsonOther, start: position, end: position + 1, depth: l.depth}) + return position + 1, true + } + lineComment := l.data[position+1] == '/' + position += 2 + nextContextCheck := position + 4_096 + for position < len(l.data) { + if lineComment && l.data[position] == '\n' { + break + } + if !lineComment && position+1 < len(l.data) && l.data[position] == '*' && l.data[position+1] == '/' { + return position + 2, true + } + if position >= nextContextCheck { + if ctx.Err() != nil { + return position, false + } + nextContextCheck = position + 4_096 + } + position++ + } + return position, true +} + +func (l *jsonLikeLexer) scanOtherToken(ctx context.Context, position int) (int, bool) { + start := position + nextContextCheck := position + 4_096 + for position < len(l.data) && !isJSONDelimiter(l.data[position]) { + if position >= nextContextCheck { + if ctx.Err() != nil { + return position, false + } + nextContextCheck = position + 4_096 + } + position++ + } + if position == start { + position++ + } + l.emit(jsonToken{kind: jsonOther, start: start, end: position, depth: l.depth}) + return position, true +} + +func (l *jsonLikeLexer) emit(token jsonToken) { + if token.kind == jsonString { + decoded, ok := decodeJSONString(l.data, token) + if !ok { + token.kind = jsonOther + } else { + token.text = decoded + } + } + if l.isSecretCandidate(token) { + l.candidates = append(l.candidates, structuredCandidate{key: l.previous[0], value: token}) + } + if len(l.tokens) < maxStructuredTokens { + l.tokens = append(l.tokens, token) + } + if l.previousCount < 2 { + l.previous[l.previousCount] = token + l.previousCount++ + return + } + l.previous[0], l.previous[1] = l.previous[1], token +} + +func (l *jsonLikeLexer) isSecretCandidate(token jsonToken) bool { + return l.previousCount == 2 && l.previous[0].kind == jsonString && + l.previous[1].kind == jsonColon && token.kind == jsonString && + l.previous[0].depth <= maxStructuredDepth && token.depth <= maxStructuredDepth && + isHighConfidenceSecretKey(l.previous[0].text) && + isContextSecretValue(l.previous[0].text, token.text) && len(l.candidates) < maxStructuredFindings +} + +func scanJSONString(ctx context.Context, data []byte, start, depth int) (jsonToken, int, bool, bool) { + i := start + 1 + nextContextCheck := i + 4_096 + tooLong := false + for i < len(data) { + if i >= nextContextCheck { + if ctx.Err() != nil { + return jsonToken{}, i, false, false + } + nextContextCheck = i + 4_096 + } + if i-start-1 > maxStructuredStringLen { + tooLong = true + } + switch data[i] { + case '\\': + if i+1 >= len(data) { + return jsonToken{}, len(data), false, true + } + i += 2 + case '"': + end := i + 1 + if tooLong || i-start-1 > maxStructuredStringLen { + return jsonToken{}, end, false, true + } + return jsonToken{ + kind: jsonString, + start: start, + end: end, + contentStart: start + 1, + contentEnd: i, + depth: depth, + }, end, true, true + default: + i++ + } + } + return jsonToken{}, len(data), false, true +} + +func decodeJSONString(data []byte, token jsonToken) (string, bool) { + encoded := data[token.start:token.end] + if !utf8.Valid(encoded) || !hasValidJSONSurrogates(encoded) { + return "", false + } + var decoded string + err := json.Unmarshal(encoded, &decoded) + return decoded, err == nil && utf8.ValidString(decoded) +} + +func hasValidJSONSurrogates(encoded []byte) bool { + for i := 1; i+1 < len(encoded); i++ { + if encoded[i] != '\\' { + continue + } + i++ + if encoded[i] != 'u' { + continue + } + first, ok := decodeHexQuad(encoded, i+1) + if !ok { + return false + } + i += 4 + switch { + case first >= 0xd800 && first <= 0xdbff: + if i+6 >= len(encoded) || encoded[i+1] != '\\' || encoded[i+2] != 'u' { + return false + } + second, secondOK := decodeHexQuad(encoded, i+3) + if !secondOK || second < 0xdc00 || second > 0xdfff { + return false + } + i += 6 + case first >= 0xdc00 && first <= 0xdfff: + return false + } + } + return true +} + +func decodeHexQuad(data []byte, start int) (uint16, bool) { + if start < 0 || start+4 > len(data) { + return 0, false + } + var value uint16 + for _, b := range data[start : start+4] { + value <<= 4 + switch { + case b >= '0' && b <= '9': + value += uint16(b - '0') + case b >= 'a' && b <= 'f': + value += uint16(b-'a') + 10 + case b >= 'A' && b <= 'F': + value += uint16(b-'A') + 10 + default: + return 0, false + } + } + return value, true +} + +func isJSONDelimiter(b byte) bool { + switch b { + case ' ', '\t', '\r', '\n', '/', '"', ':', ',', '{', '}', '[', ']': + return true + default: + return false + } +} + +type jsonPathParser struct { + tokens []jsonToken + pos int + paths map[int]string +} + +func buildJSONPaths(tokens []jsonToken) map[int]string { + p := &jsonPathParser{tokens: tokens, paths: make(map[int]string)} + for p.pos < len(tokens) { + before := p.pos + p.parseValue(nil, 0) + if p.pos == before { + p.pos++ + } + } + return p.paths +} + +func (p *jsonPathParser) parseValue(path []string, depth int) { + if p.pos >= len(p.tokens) { + return + } + if depth > maxStructuredDepth { + // Every parser call must either consume input or be at EOF. Advancing at + // the bound prevents a deeply nested non-empty array from repeatedly + // presenting the same token to parseArray. + p.pos++ + return + } + switch p.tokens[p.pos].kind { + case jsonObjectStart: + p.parseObject(path, depth+1) + case jsonArrayStart: + p.parseArray(path, depth+1) + case jsonString: + p.paths[p.tokens[p.pos].contentStart] = joinJSONPath(path) + p.pos++ + default: + p.pos++ + } +} + +func (p *jsonPathParser) parseObject(path []string, depth int) { + p.pos++ + for p.pos < len(p.tokens) { + if p.tokens[p.pos].kind == jsonObjectEnd { + p.pos++ + return + } + if p.tokens[p.pos].kind == jsonComma { + p.pos++ + continue + } + if p.tokens[p.pos].kind != jsonString { + p.pos++ + continue + } + + key := p.tokens[p.pos].text + p.pos++ + if p.pos >= len(p.tokens) || p.tokens[p.pos].kind != jsonColon { + continue + } + p.pos++ + childPath := appendJSONPath(path, key) + p.parseValue(childPath, depth) + } +} + +func (p *jsonPathParser) parseArray(path []string, depth int) { + p.pos++ + index := 0 + for p.pos < len(p.tokens) { + if p.tokens[p.pos].kind == jsonArrayEnd { + p.pos++ + return + } + if p.tokens[p.pos].kind == jsonComma { + p.pos++ + continue + } + p.parseValue(appendJSONPath(path, "["+strconv.Itoa(index)+"]"), depth) + index++ + } +} + +func appendJSONPath(path []string, component string) []string { + out := make([]string, len(path), len(path)+1) + copy(out, path) + return append(out, component) +} + +func joinJSONPath(path []string) string { + var b strings.Builder + for i, component := range path { + if strings.HasPrefix(component, "[") { + b.WriteString(component) + continue + } + if i > 0 { + b.WriteByte('.') + } + b.WriteString(safeConfigPathComponent(component)) + } + return b.String() +} + +// safeConfigPathComponent keeps untrusted structural names useful as metadata +// without allowing a secret-shaped dynamic key, control characters, or an +// attacker-sized name to be copied into output and logs. Detection continues to +// classify the original leaf key; this function affects display metadata only. +func safeConfigPathComponent(component string) string { + const maxComponentBytes = 128 + if component == "" || len(component) > maxComponentBytes || !utf8.ValidString(component) { + return "" + } + for _, r := range component { + if unicode.IsControl(r) || !unicode.IsGraphic(r) { + return "" + } + } + if len(component) >= 20 && entropy.Calculate([]byte(component)) >= minEntropy { + return "" + } + return component +} + +func isHighConfidenceSecretKey(key string) bool { + parts := splitConfigIdentifier(key) + canonical := strings.Join(parts, "") + switch canonical { + case "password", "passwd", "passphrase", "secret", + "token", + "clientsecret", "secrettoken", "accesstoken", "refreshtoken", + "authtoken", "bearertoken", "signingsecret", "webhooksecret", + "consumersecret", "appsecret", "applicationsecret", "mastersecret", + "masterkey", "encryptionkey", "privatekey": + return true + } + if len(parts) == 0 { + return false + } + switch parts[len(parts)-1] { + case "password", "passwd", "passphrase", "secret": + // Namespace prefixes (DB_PASSWORD, POSTGRES_PASSWORD, + // AzureClientSecret, JWT_SECRET) add ownership context; they do not make + // an otherwise unambiguous credential role less sensitive. + return true + } + if len(parts) >= 2 { + suffix := strings.Join(parts[len(parts)-2:], "") + switch suffix { + case "secrettoken", "accesstoken", "refreshtoken", "authtoken", "bearertoken", + "masterkey", "encryptionkey", "privatekey": + return true + } + } + return false +} + +func canonicalConfigKey(key string) string { + parts := splitConfigIdentifier(key) + return strings.Join(parts, "") +} + +func splitConfigIdentifier(value string) []string { + runes := []rune(value) + parts := make([]string, 0, 4) + start := -1 + flush := func(end int) { + if start >= 0 && end > start { + parts = append(parts, strings.ToLower(string(runes[start:end]))) + } + start = -1 + } + + for i, r := range runes { + if !unicode.IsLetter(r) && !unicode.IsDigit(r) { + flush(i) + continue + } + if start < 0 { + start = i + continue + } + prev := runes[i-1] + nextLower := i+1 < len(runes) && unicode.IsLower(runes[i+1]) + if unicode.IsUpper(r) && (unicode.IsLower(prev) || unicode.IsDigit(prev) || (unicode.IsUpper(prev) && nextLower)) { + flush(i) + start = i + } + } + flush(len(runes)) + return parts +} + +func isContextSecretValue(key, value string) bool { + trimmed := strings.TrimSpace(value) + if len(trimmed) < minContextSecretLength || len(trimmed) > maxStructuredStringLen { + return false + } + lower := strings.ToLower(trimmed) + switch lower { + case "password", "pass", "secret", "string", "redacted", "masked", "changeme", "change-me", "change_me", "dummy", + "example", "sample", "notset", "not-set", "none", "null", "default", "test", "dev", "local", + "true", "false", "yes", "no", "on", "off", + "your-secret", "your_secret", "your-password", "your_password", "your_key_here", "your-key-here", + "replace_me", "todo", "fixme", "placeholder", "example-secret": + return false + } + if isDegenerateValue([]byte(trimmed)) || isBareReference([]byte(trimmed)) { + return false + } + if isVersionLikeValue(trimmed) { + return false + } + if isExternalSecretReference(lower, trimmed) { + return false + } + if isKeyMaterialReference(canonicalConfigKey(key), lower) { + return false + } + if !plausibleStructuredSecretValue(trimmed) { + return false + } + return true +} + +func isVersionLikeValue(value string) bool { + candidate := strings.TrimLeft(value, "vV^~<>= ") + core := candidate + if suffix := strings.IndexAny(core, "-+"); suffix >= 0 { + core = core[:suffix] + } + parts := strings.Split(core, ".") + if len(parts) < 2 { + return false + } + for _, part := range parts { + if part == "" { + return false + } + for _, char := range part { + if !unicode.IsDigit(char) && char != 'x' && char != 'X' && char != '*' { + return false + } + } + } + return true +} + +func plausibleStructuredSecretValue(value string) bool { + letters := 0 + hasDigit := false + hasSymbol := false + hasWhitespace := false + for _, r := range value { + switch { + case unicode.IsLetter(r): + letters++ + case unicode.IsDigit(r): + hasDigit = true + case unicode.IsSpace(r): + hasWhitespace = true + default: + hasSymbol = true + } + } + // Locale labels and prose instructions are overwhelmingly alphabetic words + // separated by spaces. Passphrases containing digits or punctuation remain + // eligible, as do compact credentials. + if hasWhitespace && !hasDigit && !hasSymbol { + return false + } + if len(value) >= 8 && entropy.Calculate([]byte(value)) < 2.5 { + return false + } + if letters >= minVowelRatioLetters && !hasDigit && !hasSymbol && hasHighVowelRatio([]byte(value)) { + return false + } + return true +} + +func isExternalSecretReference(lower, original string) bool { + if (strings.HasPrefix(original, "${") && strings.HasSuffix(original, "}")) || + (strings.HasPrefix(original, "{{") && strings.HasSuffix(original, "}}")) || + (strings.HasPrefix(original, "%") && strings.HasSuffix(original, "%")) || + (strings.HasPrefix(original, "$(") && strings.HasSuffix(original, ")")) || + isDollarReference(original) || isAngleReference(original) { + return true + } + if isYAMLAliasReference(original) { + return true + } + if isFilesystemReference(lower) { + return true + } + for _, prefix := range []string{ + "env:", "secret://", "vault://", "kv://", "keyvault://", + "aws-secretsmanager://", "gcp-secretmanager://", "op://", "1password://", + "@microsoft.keyvault(", "os.getenv(", "os.environ[", "process.env.", + "environment.getenvironmentvariable(", "system.getenv(", "config.get(", + } { + if strings.HasPrefix(lower, prefix) { + return true + } + } + return false +} + +func isYAMLAliasReference(value string) bool { + if len(value) < 2 || value[0] != '*' { + return false + } + for _, r := range value[1:] { + if !unicode.IsLetter(r) && !unicode.IsDigit(r) && r != '_' && r != '-' { + return false + } + } + return true +} + +func isFilesystemReference(lower string) bool { + for _, prefix := range []string{ + "/", "./", "../", "~/", "\\\\", "file:", + "secrets/", ".secrets/", "certs/", "certificates/", "run/secrets/", "var/run/secrets/", + } { + if strings.HasPrefix(lower, prefix) { + return true + } + } + if len(lower) >= 3 && lower[1] == ':' && lower[0] >= 'a' && lower[0] <= 'z' && + (lower[2] == '/' || lower[2] == '\\') { + return true + } + if !strings.ContainsAny(lower, "/\\") { + return false + } + for _, suffix := range []string{ + ".pem", ".key", ".p12", ".pfx", ".jks", ".keystore", ".crt", ".cer", + } { + if strings.HasSuffix(lower, suffix) { + return true + } + } + return false +} + +func isDollarReference(value string) bool { + if !strings.HasPrefix(value, "$") || len(value) < 2 { + return false + } + for _, r := range value[1:] { + if !unicode.IsLetter(r) && !unicode.IsDigit(r) && r != '_' && r != ':' { + return false + } + } + return true +} + +func isAngleReference(value string) bool { + if len(value) < 3 || value[0] != '<' || value[len(value)-1] != '>' { + return false + } + for _, r := range value[1 : len(value)-1] { + if !unicode.IsLetter(r) && !unicode.IsDigit(r) && r != '_' && r != '-' && r != ' ' { + return false + } + } + return true +} + +func isKeyMaterialReference(key, lower string) bool { + switch key { + case "privatekey", "masterkey", "encryptionkey": + default: + return false + } + for _, prefix := range []string{ + "pkcs11:", "arn:aws:kms:", "alias/", + } { + if strings.HasPrefix(lower, prefix) { + return true + } + } + return false +} + +func minInt(a, b int) int { + if a < b { + return a + } + return b +} + +func init() { + detector.Register(&StructuredConfigDetector{}) +} diff --git a/internal/detector/generic/structured_config_formats.go b/internal/detector/generic/structured_config_formats.go new file mode 100644 index 0000000..93d0a6f --- /dev/null +++ b/internal/detector/generic/structured_config_formats.go @@ -0,0 +1,1045 @@ +package generic + +import ( + "bytes" + "context" + "encoding/xml" + "io" + "log/slog" + "strconv" + "strings" + "unicode" + "unicode/utf8" + + "github.com/HodeTech/leakwatch/internal/detector" + "github.com/pelletier/go-toml/v2" + "gopkg.in/yaml.v3" +) + +type lineConfigFormat string + +const ( + formatDotenv lineConfigFormat = "dotenv" + formatTOML lineConfigFormat = "toml" + formatYAML lineConfigFormat = "yaml" +) + +type lineAssignment struct { + key string + inlinePath []string + value string + valueStart, valueEnd int + indent int + hasValue bool + blockStyle byte +} + +type yamlSignal struct { + indent int + hasValue bool +} + +func (d *StructuredConfigDetector) scanLineConfig(ctx context.Context, data []byte) []detector.RawFinding { + if !utf8.Valid(data) || ctx.Err() != nil { + return nil + } + format, confident := detectLineConfigFormat(ctx, data) + if !confident { + return nil + } + scanner := lineConfigScanner{detector: d, ctx: ctx, data: data, format: format} + return scanner.scan() +} + +type lineConfigScanner struct { + detector *StructuredConfigDetector + ctx context.Context + data []byte + format lineConfigFormat + findings []detector.RawFinding + yamlPath []yamlPathComponent + tomlPath []string +} + +func (s *lineConfigScanner) scan() []detector.RawFinding { + s.findings = make([]detector.RawFinding, 0, 4) + for offset := 0; offset < len(s.data); { + lineEnd, complete := contextLineEnd(s.ctx, s.data, offset) + if !complete { + return nil + } + offset = s.processLine(offset, lineEnd) + } + return s.findings +} + +func (s *lineConfigScanner) processLine(offset, lineEnd int) int { + nextOffset := nextLineOffset(lineEnd, len(s.data)) + line := bytes.TrimSuffix(s.data[offset:lineEnd], []byte{'\r'}) + if s.format == formatTOML { + if section, ok := parseTOMLSection(line); ok { + s.tomlPath = section + return nextOffset + } + } + assignment, ok := parseLineAssignment(line, offset, s.format) + if !ok { + return nextOffset + } + path, terminal := s.assignmentPath(assignment) + if terminal { + return nextOffset + } + if s.format == formatYAML && assignment.blockStyle != 0 { + var scalarOK bool + assignment.value, assignment.valueStart, assignment.valueEnd, nextOffset, scalarOK = parseYAMLBlockScalar(s.ctx, s.data, nextOffset, assignment.indent, assignment.blockStyle) + if !scalarOK { + return nextOffset + } + } + s.appendFinding(path, assignment) + return nextOffset +} + +func (s *lineConfigScanner) assignmentPath(assignment lineAssignment) ([]string, bool) { + switch s.format { + case formatYAML: + for len(s.yamlPath) > 0 && s.yamlPath[len(s.yamlPath)-1].indent >= assignment.indent { + s.yamlPath = s.yamlPath[:len(s.yamlPath)-1] + } + path := yamlComponents(s.yamlPath) + if !assignment.hasValue { + s.yamlPath = append(s.yamlPath, yamlPathComponent{indent: assignment.indent, key: assignment.key}) + return path, true + } + return path, false + case formatTOML: + return append(append([]string(nil), s.tomlPath...), assignment.inlinePath...), false + default: + return nil, false + } +} + +func (s *lineConfigScanner) appendFinding(path []string, assignment lineAssignment) { + if !assignment.hasValue || !isHighConfidenceSecretKey(assignment.key) || + !isContextSecretValue(assignment.key, assignment.value) || len(s.findings) >= maxStructuredFindings { + return + } + raw := bytes.Clone(s.data[assignment.valueStart:assignment.valueEnd]) + s.findings = append(s.findings, detector.RawFinding{ + DetectorID: s.detector.ID(), Raw: raw, Redacted: detector.RedactBytes(raw), + ExtraData: map[string]string{ + "key_name": assignment.key, "key_path": joinJSONPath(appendJSONPath(path, assignment.key)), + "config_format": string(s.format), + }, + ByteStart: assignment.valueStart, ByteEnd: assignment.valueEnd, + }) +} + +func detectLineConfigFormat(ctx context.Context, data []byte) (lineConfigFormat, bool) { + signals := lineConfigSignals{pendingYAMLParentIndent: -1} + for offset := 0; offset < len(data); { + lineEnd, complete := contextLineEnd(ctx, data, offset) + if !complete { + return "", false + } + line := bytes.TrimSpace(data[offset:lineEnd]) + if len(line) == 0 || bytes.HasPrefix(line, []byte("#")) || bytes.HasPrefix(line, []byte("//")) { + offset = nextLineOffset(lineEnd, len(data)) + continue + } + if section, ok := parseTOMLSection(line); ok && len(section) > 0 { + return formatTOML, true + } + if bytes.Equal(line, []byte("---")) { + signals.hasYAMLDocumentMarker = true + offset = nextLineOffset(lineEnd, len(data)) + continue + } + signals.observe(data[offset:lineEnd], line, offset) + offset = nextLineOffset(lineEnd, len(data)) + } + return signals.detectedFormat(data) +} + +type lineConfigSignals struct { + yamlSignalCount, dotenvAssignments, tomlAssignments int + pendingYAMLParentIndent int + yamlNested, yamlInvalid, dotenvInvalid, tomlInvalid bool + hasYAMLDocumentMarker bool + hasPlausibleYAMLSecret bool +} + +func (s *lineConfigSignals) observe(rawLine, trimmedLine []byte, offset int) { + signal, yamlMapping := yamlMappingSignal(rawLine) + switch { + case yamlMapping: + s.observeYAML(signal) + if assignment, parsed := parseLineAssignment(rawLine, offset, formatYAML); parsed && + assignment.hasValue && isHighConfidenceSecretKey(assignment.key) && + isContextSecretValue(assignment.key, assignment.value) { + s.hasPlausibleYAMLSecret = true + } + s.dotenvInvalid = true + case isDotenvAssignment(trimmedLine): + s.dotenvAssignments++ + s.yamlInvalid = true + case isNeutralDotenvLine(trimmedLine): + // Empty placeholders are valid and common in committed .env templates. + // They are neutral evidence and must not invalidate later assignments. + s.yamlInvalid = true + default: + s.dotenvInvalid, s.yamlInvalid = true, true + } + if _, ok := parseLineAssignment(rawLine, offset, formatTOML); ok { + s.tomlAssignments++ + } else { + s.tomlInvalid = true + } +} + +func (s *lineConfigSignals) observeYAML(signal yamlSignal) { + s.yamlSignalCount++ + if s.pendingYAMLParentIndent >= 0 { + if signal.indent > s.pendingYAMLParentIndent { + s.yamlNested = true + } + if signal.indent <= s.pendingYAMLParentIndent { + s.pendingYAMLParentIndent = -1 + } + } + if !signal.hasValue { + s.pendingYAMLParentIndent = signal.indent + } +} + +func (s lineConfigSignals) detectedFormat(data []byte) (lineConfigFormat, bool) { + hasYAMLEvidence := (s.hasYAMLDocumentMarker && s.yamlSignalCount > 0) || s.yamlNested || + (s.yamlSignalCount >= 2 && !s.yamlInvalid && s.hasPlausibleYAMLSecret) + if hasYAMLEvidence && isStructuredYAMLDocument(data) { + return formatYAML, true + } + if s.dotenvAssignments > 0 && !s.dotenvInvalid && s.yamlSignalCount == 0 { + return formatDotenv, true + } + // Without source-extension metadata a lone `password = "..."` line is + // indistinguishable from source code. Requiring a complete, multi-assignment + // document preserves support for sectionless TOML while keeping that common + // code shape out of the high-confidence detector. + if s.tomlAssignments >= 2 && !s.tomlInvalid && s.yamlSignalCount == 0 { + return formatTOML, true + } + return "", false +} + +func isStructuredYAMLDocument(data []byte) bool { + decoder := yaml.NewDecoder(bytes.NewReader(data)) + documents := 0 + for { + var document yaml.Node + err := decoder.Decode(&document) + if err == io.EOF { + return documents > 0 + } + if err != nil || len(document.Content) == 0 { + return false + } + root := document.Content[0] + if root.Kind != yaml.MappingNode && root.Kind != yaml.SequenceNode { + return false + } + documents++ + if documents > 128 { + return false + } + } +} + +func contextLineEnd(ctx context.Context, data []byte, offset int) (int, bool) { + const checkBytes = 4 * 1024 + for start := offset; start < len(data); { + if ctx.Err() != nil { + return 0, false + } + end := minInt(start+checkBytes, len(data)) + if relative := bytes.IndexByte(data[start:end], '\n'); relative >= 0 { + return start + relative, true + } + start = end + } + return len(data), ctx.Err() == nil +} + +func yamlMappingSignal(line []byte) (yamlSignal, bool) { + trimmed := bytes.TrimLeft(line, " \t") + indent := visualIndent(line[:len(line)-len(trimmed)]) + if bytes.HasPrefix(trimmed, []byte("- ")) { + trimmed = bytes.TrimLeft(trimmed[2:], " \t") + } + index := bytes.IndexByte(trimmed, ':') + if index <= 0 || !isPlainConfigKey(bytes.TrimSpace(trimmed[:index])) { + return yamlSignal{}, false + } + remainder := bytes.TrimSpace(trimmed[index+1:]) + return yamlSignal{indent: indent, hasValue: len(remainder) > 0 && remainder[0] != '#'}, true +} + +func hasNestedYAMLSignals(signals []yamlSignal) bool { + for i, parent := range signals { + if parent.hasValue { + continue + } + for _, child := range signals[i+1:] { + if child.indent > parent.indent { + return true + } + if child.indent <= parent.indent { + break + } + } + } + return false +} + +func isDotenvAssignment(line []byte) bool { + line = bytes.TrimSpace(line) + if bytes.HasPrefix(line, []byte("export ")) { + line = bytes.TrimSpace(line[len("export "):]) + } + index := bytes.IndexByte(line, '=') + if index <= 0 { + return false + } + key := bytes.TrimSpace(line[:index]) + if !isDotenvKey(key) { + return false + } + return len(bytes.TrimSpace(line[index+1:])) > 0 +} + +func isNeutralDotenvLine(line []byte) bool { + line = bytes.TrimSpace(line) + if bytes.HasPrefix(line, []byte("export ")) { + line = bytes.TrimSpace(line[len("export "):]) + } + index := bytes.IndexByte(line, '=') + if index <= 0 || !isShellIdentifier(bytes.TrimSpace(line[:index])) { + return false + } + key := bytes.TrimSpace(line[:index]) + return len(bytes.TrimSpace(line[index+1:])) == 0 || key[0] < 'A' || key[0] > 'Z' +} + +func isDotenvKey(key []byte) bool { + if len(key) == 0 || key[0] < 'A' || key[0] > 'Z' { + return false + } + for _, b := range key[1:] { + if (b < 'A' || b > 'Z') && (b < '0' || b > '9') && b != '_' { + return false + } + } + return true +} + +func isShellIdentifier(key []byte) bool { + if len(key) == 0 || !isASCIIAlpha(key[0]) && key[0] != '_' { + return false + } + for _, b := range key[1:] { + if !isASCIIAlpha(b) && (b < '0' || b > '9') && b != '_' { + return false + } + } + return true +} + +func isASCIIAlpha(value byte) bool { + return value >= 'A' && value <= 'Z' || value >= 'a' && value <= 'z' +} + +func parseLineAssignment(line []byte, base int, format lineConfigFormat) (lineAssignment, bool) { + trimmed, indentBytes, originalIndent, ok := normalizeAssignmentLine(line, format) + if !ok { + return lineAssignment{}, false + } + keyPath, delimiterIndex, ok := assignmentKeyPath(trimmed, format) + if !ok { + return lineAssignment{}, false + } + key := keyPath[len(keyPath)-1] + inlinePath := keyPath[:len(keyPath)-1] + valueBase := base + indentBytes + delimiterIndex + 1 + valueBytes := trimmed[delimiterIndex+1:] + return assignmentFromValue(key, inlinePath, valueBytes, valueBase, originalIndent, format) +} + +func normalizeAssignmentLine(line []byte, format lineConfigFormat) ([]byte, int, int, bool) { + trimmed := bytes.TrimLeft(line, " \t") + indentBytes := len(line) - len(trimmed) + originalIndent := visualIndent(line[:indentBytes]) + if len(trimmed) == 0 || trimmed[0] == '#' || trimmed[0] == ';' || bytes.HasPrefix(trimmed, []byte("//")) { + return nil, 0, 0, false + } + prefix := "" + if format == formatYAML && bytes.HasPrefix(trimmed, []byte("- ")) { + prefix = "- " + } else if format == formatDotenv && bytes.HasPrefix(trimmed, []byte("export ")) { + prefix = "export " + } + if prefix != "" { + rest := trimmed[len(prefix):] + withoutSpacing := bytes.TrimLeft(rest, " \t") + indentBytes += len(prefix) + len(rest) - len(withoutSpacing) + trimmed = withoutSpacing + } + return trimmed, indentBytes, originalIndent, true +} + +func assignmentKeyPath(trimmed []byte, format lineConfigFormat) ([]string, int, bool) { + delimiter := byte('=') + if format == formatYAML { + delimiter = ':' + } + index := bytes.IndexByte(trimmed, delimiter) + if index <= 0 { + return nil, 0, false + } + keyBytes := bytes.TrimSpace(trimmed[:index]) + if format == formatTOML { + path, ok := parseTOMLDottedKey(keyBytes) + return path, index, ok + } + if !isPlainConfigKey(keyBytes) { + return nil, 0, false + } + return []string{string(keyBytes)}, index, true +} + +func assignmentFromValue( + key string, + inlinePath []string, + valueBytes []byte, + valueBase, originalIndent int, + format lineConfigFormat, +) (lineAssignment, bool) { + trimmedValue := bytes.TrimSpace(valueBytes) + if len(trimmedValue) == 0 || (format == formatYAML && trimmedValue[0] == '#') { + return lineAssignment{key: key, inlinePath: inlinePath, indent: originalIndent}, true + } + if format == formatYAML && (trimmedValue[0] == '|' || trimmedValue[0] == '>') { + if !isYAMLBlockIndicator(trimmedValue) { + return lineAssignment{}, false + } + return lineAssignment{ + key: key, inlinePath: inlinePath, indent: originalIndent, + hasValue: true, blockStyle: trimmedValue[0], + }, true + } + value, start, end, ok := parseLineScalar(valueBytes, valueBase, format) + if !ok { + return lineAssignment{}, false + } + return lineAssignment{ + key: key, + inlinePath: inlinePath, + value: value, + valueStart: start, + valueEnd: end, + indent: originalIndent, + hasValue: true, + }, true +} + +func parseTOMLDottedKey(raw []byte) ([]string, bool) { + parts := bytes.Split(raw, []byte(".")) + if len(parts) == 0 { + return nil, false + } + result := make([]string, 0, len(parts)) + for _, part := range parts { + part = bytes.TrimSpace(part) + if len(part) == 0 { + return nil, false + } + if (part[0] == '\'' || part[0] == '"') && len(part) >= 2 && part[len(part)-1] == part[0] { + value, _, _, ok := parseLineScalar(part, 0, formatTOML) + if !ok || value == "" { + return nil, false + } + result = append(result, value) + continue + } + if !isPlainConfigKey(part) { + return nil, false + } + result = append(result, string(part)) + } + return result, true +} + +func isYAMLBlockIndicator(value []byte) bool { + value = bytes.TrimSpace(value) + if len(value) == 0 || (value[0] != '|' && value[0] != '>') { + return false + } + i := 1 + for i < len(value) && (value[i] == '+' || value[i] == '-' || (value[i] >= '1' && value[i] <= '9')) { + i++ + } + rest := bytes.TrimSpace(value[i:]) + return len(rest) == 0 || rest[0] == '#' +} + +func parseYAMLBlockScalar( + ctx context.Context, + data []byte, + offset int, + parentIndent int, + style byte, +) (string, int, int, int, bool) { + lines, contentIndentBytes, nextOffset, ok := collectYAMLBlockLines(ctx, data, offset, parentIndent) + if !ok { + return "", 0, 0, nextOffset, false + } + decoded, first, last, ok := decodeYAMLBlockLines(data, lines, contentIndentBytes, style) + return decoded, first, last, nextOffset, ok +} + +type yamlBlockLine struct { + start, end int + indentBytes int + blank bool +} + +func collectYAMLBlockLines( + ctx context.Context, + data []byte, + offset, parentIndent int, +) ([]yamlBlockLine, int, int, bool) { + lines := make([]yamlBlockLine, 0, 4) + contentIndentBytes := -1 + for offset < len(data) { + lineEnd, complete := contextLineEnd(ctx, data, offset) + if !complete { + return nil, 0, len(data), false + } + line := data[offset:lineEnd] + if len(line) > 0 && line[len(line)-1] == '\r' { + line = line[:len(line)-1] + } + trimmed := bytes.TrimSpace(line) + indentBytes := len(line) - len(bytes.TrimLeft(line, " \t")) + if bytes.IndexByte(line[:indentBytes], '\t') >= 0 { + return nil, 0, offset, false + } + indent := visualIndent(line[:indentBytes]) + if len(trimmed) > 0 && indent <= parentIndent { + break + } + if len(trimmed) > 0 { + if contentIndentBytes < 0 || indentBytes < contentIndentBytes { + contentIndentBytes = indentBytes + } + } + lines = append(lines, yamlBlockLine{ + start: offset, end: offset + len(line), indentBytes: indentBytes, blank: len(trimmed) == 0, + }) + offset = nextLineOffset(lineEnd, len(data)) + } + if contentIndentBytes <= parentIndent || len(lines) == 0 { + return nil, 0, offset, false + } + return lines, contentIndentBytes, offset, true +} + +func decodeYAMLBlockLines(data []byte, lines []yamlBlockLine, contentIndentBytes int, style byte) (string, int, int, bool) { + first, last := -1, -1 + var decoded strings.Builder + previousBlank := false + for _, line := range lines { + if line.blank { + if decoded.Len() > 0 { + decoded.WriteByte('\n') + } + previousBlank = true + continue + } + contentStart := line.start + contentIndentBytes + if contentStart > line.end { + return "", 0, 0, false + } + if first < 0 { + first = contentStart + } + if decoded.Len() > 0 && !previousBlank { + if style == '|' { + decoded.WriteByte('\n') + } else { + decoded.WriteByte(' ') + } + } + decoded.Write(data[contentStart:line.end]) + last = line.end + previousBlank = false + } + if first < 0 || last <= first || decoded.Len() > maxStructuredStringLen { + return "", 0, 0, false + } + return decoded.String(), first, last, true +} + +func parseLineScalar(value []byte, base int, format lineConfigFormat) (string, int, int, bool) { + left := len(value) - len(bytes.TrimLeft(value, " \t")) + value = value[left:] + base += left + if len(value) == 0 || value[0] == '|' || value[0] == '>' || value[0] == '[' || value[0] == '{' { + return "", 0, 0, false + } + if format == formatYAML && value[0] == '&' { + anchorEnd := bytes.IndexAny(value, " \t") + if anchorEnd <= 1 { + return "", 0, 0, false + } + rest := value[anchorEnd:] + return parseLineScalar(rest, base+anchorEnd, format) + } + if value[0] == '\'' || value[0] == '"' { + return parseQuotedLineScalar(value, base, format) + } + return parsePlainLineScalar(value, base, format) +} + +func parseQuotedLineScalar(value []byte, base int, format lineConfigFormat) (string, int, int, bool) { + quote := value[0] + end := findClosingQuoteForFormat(value, quote, format) + if end <= 0 || !onlyWhitespaceOrComment(value[end+1:]) { + return "", 0, 0, false + } + raw := value[1:end] + if len(raw) > maxStructuredStringLen { + return "", 0, 0, false + } + decoded, ok := decodeQuotedLineScalar(value[:end+1], raw, quote, format) + if !ok { + return "", 0, 0, false + } + return decoded, base + 1, base + end, true +} + +func decodeQuotedLineScalar(quoted, raw []byte, quote byte, format lineConfigFormat) (string, bool) { + if quote == '\'' { + decoded := string(raw) + if format == formatYAML { + decoded = strings.ReplaceAll(decoded, "''", "'") + } + return decoded, true + } + var decoded string + var err error + switch format { + case formatYAML: + err = yaml.Unmarshal(quoted, &decoded) + case formatTOML: + var scalar struct { + Value string `toml:"value"` + } + err = toml.Unmarshal(append([]byte("value = "), quoted...), &scalar) + decoded = scalar.Value + default: + decoded, err = strconv.Unquote(string(quoted)) + } + return decoded, err == nil +} + +func parsePlainLineScalar(value []byte, base int, format lineConfigFormat) (string, int, int, bool) { + end := len(value) + for i := 1; i < len(value); i++ { + if value[i] == '#' && (value[i-1] == ' ' || value[i-1] == '\t') { + end = i + break + } + } + raw := bytes.TrimRight(value[:end], " \t\r") + if len(raw) == 0 || len(raw) > maxStructuredStringLen || format == formatTOML { + return "", 0, 0, false + } + return string(raw), base, base + len(raw), true +} + +func findClosingQuote(value []byte, quote byte) int { + return findClosingQuoteForFormat(value, quote, "") +} + +func findClosingQuoteForFormat(value []byte, quote byte, format lineConfigFormat) int { + for i := 1; i < len(value); i++ { + if value[i] != quote { + continue + } + if quote == '\'' && format == formatYAML && + i+1 < len(value) && value[i+1] == quote { + i++ + continue + } + if quote == '"' { + backslashes := 0 + for j := i - 1; j >= 0 && value[j] == '\\'; j-- { + backslashes++ + } + if backslashes%2 == 1 { + continue + } + } + return i + } + return -1 +} + +func onlyWhitespaceOrComment(value []byte) bool { + value = bytes.TrimSpace(value) + return len(value) == 0 || value[0] == '#' +} + +func isPlainConfigKey(value []byte) bool { + if len(value) == 0 { + return false + } + for _, r := range string(value) { + if !unicode.IsLetter(r) && !unicode.IsDigit(r) && r != '_' && r != '-' && r != '.' { + return false + } + } + return true +} + +func parseTOMLSection(line []byte) ([]string, bool) { + trimmed := bytes.TrimSpace(line) + if comment := bytes.IndexByte(trimmed, '#'); comment >= 0 { + trimmed = bytes.TrimSpace(trimmed[:comment]) + } + if len(trimmed) < 3 || trimmed[0] != '[' || trimmed[len(trimmed)-1] != ']' || trimmed[1] == '[' { + return nil, false + } + section := strings.TrimSpace(string(trimmed[1 : len(trimmed)-1])) + if section == "" { + return nil, false + } + parts := strings.Split(section, ".") + for i := range parts { + parts[i] = strings.TrimSpace(parts[i]) + if !isPlainConfigKey([]byte(parts[i])) { + return nil, false + } + } + return parts, true +} + +type yamlPathComponent struct { + indent int + key string +} + +func yamlComponents(path []yamlPathComponent) []string { + out := make([]string, len(path)) + for i := range path { + out[i] = path[i].key + } + return out +} + +func visualIndent(prefix []byte) int { + indent := 0 + for _, b := range prefix { + if b == '\t' { + indent += 8 + } else { + indent++ + } + } + return indent +} + +func nextLineOffset(lineEnd, dataLen int) int { + if lineEnd < dataLen { + return lineEnd + 1 + } + return dataLen +} + +type xmlFrame struct { + name string + path []string + contentStart int + textStart int + textEnd int + text []byte + hasChild bool + validText bool +} + +type xmlAttributeSpan struct { + name string + value string + valueStart int + valueEnd int +} + +func (d *StructuredConfigDetector) scanXML(ctx context.Context, data []byte) []detector.RawFinding { + if !utf8.Valid(data) || ctx.Err() != nil { + return nil + } + decoder := xml.NewDecoder(bytes.NewReader(data)) + decoder.Strict = true + state := xmlScanState{detector: d, data: data, stack: make([]xmlFrame, 0, 8), findings: make([]detector.RawFinding, 0, 4)} + tokenCount := 0 + for { + if tokenCount&255 == 0 && ctx.Err() != nil { + return nil + } + if tokenCount >= maxStructuredTokens { + slog.WarnContext(ctx, "structured config XML scan truncated at token limit", + slog.Int("token_limit", maxStructuredTokens), + slog.Int("findings_preserved", len(state.findings))) + return state.findings + } + tokenStart := int(decoder.InputOffset()) + token, err := decoder.Token() + if err == io.EOF { + break + } + if err != nil { + return nil + } + tokenCount++ + if !state.consume(token, tokenStart, int(decoder.InputOffset())) { + slog.WarnContext(ctx, "structured config XML scan truncated at structural limit", + slog.Int("depth_limit", maxStructuredDepth), + slog.Int("findings_preserved", len(state.findings))) + return state.findings + } + } + if len(state.stack) != 0 { + return nil + } + return state.findings +} + +type xmlScanState struct { + detector *StructuredConfigDetector + data []byte + stack []xmlFrame + findings []detector.RawFinding +} + +func (s *xmlScanState) consume(token xml.Token, start, end int) bool { + switch value := token.(type) { + case xml.StartElement: + return s.pushElement(value, start, end) + case xml.CharData: + s.appendCharacterData(value, start, end) + case xml.Comment, xml.Directive, xml.ProcInst: + s.invalidateCurrentText() + case xml.EndElement: + return s.closeElement(value) + } + return true +} + +func (s *xmlScanState) pushElement(element xml.StartElement, start, end int) bool { + if len(s.stack) >= maxStructuredDepth { + return false + } + path := []string(nil) + if len(s.stack) > 0 { + s.stack[len(s.stack)-1].hasChild = true + path = append(path, s.stack[len(s.stack)-1].path...) + } + path = append(path, element.Name.Local) + attributes, ok := parseXMLAttributeSpans(s.data[start:end], start, element.Attr) + if !ok { + return false + } + s.findings = appendXMLAttributeFindings(s.detector, s.findings, s.data, path, attributes) + s.stack = append(s.stack, xmlFrame{ + name: element.Name.Local, path: path, contentStart: end, textStart: -1, validText: true, + }) + return true +} + +func (s *xmlScanState) appendCharacterData(value xml.CharData, start, end int) { + if len(s.stack) == 0 { + return + } + frame := &s.stack[len(s.stack)-1] + if bytes.HasPrefix(s.data[start:end], []byte("")) { + start += len("") + } + if frame.textStart < 0 { + frame.textStart = start + } + frame.textEnd = end + frame.text = append(frame.text, value...) +} + +func (s *xmlScanState) invalidateCurrentText() { + if len(s.stack) > 0 { + s.stack[len(s.stack)-1].validText = false + } +} + +func (s *xmlScanState) closeElement(element xml.EndElement) bool { + if len(s.stack) == 0 || s.stack[len(s.stack)-1].name != element.Name.Local { + return false + } + frame := s.stack[len(s.stack)-1] + s.stack = s.stack[:len(s.stack)-1] + if frame.hasChild || !frame.validText || frame.textStart < frame.contentStart { + return true + } + decoded := strings.TrimSpace(string(frame.text)) + start, end := trimSpaceBounds(s.data, frame.textStart, frame.textEnd) + if start >= end || !isHighConfidenceSecretKey(frame.name) || + !isContextSecretValue(frame.name, decoded) || len(s.findings) >= maxStructuredFindings { + return true + } + raw := bytes.Clone(s.data[start:end]) + s.findings = append(s.findings, detector.RawFinding{ + DetectorID: s.detector.ID(), Raw: raw, Redacted: detector.RedactBytes(raw), + ExtraData: map[string]string{ + "key_name": frame.name, "key_path": joinJSONPath(frame.path), "config_format": "xml", + }, + ByteStart: start, ByteEnd: end, + }) + return true +} + +func parseXMLAttributeSpans(raw []byte, base int, decoded []xml.Attr) ([]xmlAttributeSpan, bool) { + if len(raw) < 3 || raw[0] != '<' { + return nil, false + } + i := xmlNameEnd(raw, 1) + spans := make([]xmlAttributeSpan, 0, len(decoded)) + for i < len(raw) { + i = skipXMLSpace(raw, i) + if i >= len(raw) || raw[i] == '>' || raw[i] == '/' { + break + } + if len(spans) >= len(decoded) { + return nil, false + } + span, next, ok := parseXMLAttributeSpan(raw, base, i, decoded[len(spans)]) + if !ok { + return nil, false + } + spans = append(spans, span) + i = next + } + return spans, len(spans) == len(decoded) +} + +func xmlNameEnd(raw []byte, start int) int { + for start < len(raw) && !isXMLSpace(raw[start]) && raw[start] != '>' && raw[start] != '/' { + start++ + } + return start +} + +func skipXMLSpace(raw []byte, start int) int { + for start < len(raw) && isXMLSpace(raw[start]) { + start++ + } + return start +} + +func parseXMLAttributeSpan(raw []byte, base, start int, decoded xml.Attr) (xmlAttributeSpan, int, bool) { + nameEnd := start + for nameEnd < len(raw) && !isXMLSpace(raw[nameEnd]) && raw[nameEnd] != '=' && raw[nameEnd] != '>' && raw[nameEnd] != '/' { + nameEnd++ + } + if nameEnd == start { + return xmlAttributeSpan{}, 0, false + } + valueStart := skipXMLSpace(raw, nameEnd) + if valueStart >= len(raw) || raw[valueStart] != '=' { + return xmlAttributeSpan{}, 0, false + } + valueStart = skipXMLSpace(raw, valueStart+1) + if valueStart >= len(raw) || (raw[valueStart] != '\'' && raw[valueStart] != '"') { + return xmlAttributeSpan{}, 0, false + } + quote := raw[valueStart] + valueStart++ + closingOffset := bytes.IndexByte(raw[valueStart:], quote) + if closingOffset < 0 { + return xmlAttributeSpan{}, 0, false + } + valueEnd := valueStart + closingOffset + return xmlAttributeSpan{ + name: decoded.Name.Local, value: decoded.Value, + valueStart: base + valueStart, valueEnd: base + valueEnd, + }, valueEnd + 1, true +} + +func appendXMLAttributeFindings( + d *StructuredConfigDetector, + findings []detector.RawFinding, + data []byte, + path []string, + attributes []xmlAttributeSpan, +) []detector.RawFinding { + appendFinding := func(key string, span xmlAttributeSpan, keyPath []string) { + if len(findings) >= maxStructuredFindings || !isHighConfidenceSecretKey(key) || + !isContextSecretValue(key, span.value) || span.valueStart >= span.valueEnd { + return + } + raw := bytes.Clone(data[span.valueStart:span.valueEnd]) + findings = append(findings, detector.RawFinding{ + DetectorID: d.ID(), Raw: raw, Redacted: detector.RedactBytes(raw), + ExtraData: map[string]string{ + "key_name": key, "key_path": joinJSONPath(keyPath), "config_format": "xml", + }, + ByteStart: span.valueStart, ByteEnd: span.valueEnd, + }) + } + + for _, attribute := range attributes { + if isHighConfidenceSecretKey(attribute.name) { + appendFinding(attribute.name, attribute, appendJSONPath(path, "@"+attribute.name)) + } + } + var semanticKey string + var valueAttribute *xmlAttributeSpan + for i := range attributes { + switch strings.ToLower(attributes[i].name) { + case "key", "name": + if isHighConfidenceSecretKey(attributes[i].value) { + semanticKey = attributes[i].value + } + case "value", "secret": + valueAttribute = &attributes[i] + } + } + if semanticKey != "" && valueAttribute != nil { + appendFinding(semanticKey, *valueAttribute, appendJSONPath(path, semanticKey)) + } + return findings +} + +func isXMLSpace(value byte) bool { + return value == ' ' || value == '\t' || value == '\r' || value == '\n' +} + +func trimSpaceBounds(data []byte, start, end int) (int, int) { + for start < end && isASCIISpace(data[start]) { + start++ + } + for end > start && isASCIISpace(data[end-1]) { + end-- + } + return start, end +} + +func isASCIISpace(b byte) bool { + return b == ' ' || b == '\t' || b == '\r' || b == '\n' +} diff --git a/internal/detector/generic/structured_config_test.go b/internal/detector/generic/structured_config_test.go new file mode 100644 index 0000000..c951c38 --- /dev/null +++ b/internal/detector/generic/structured_config_test.go @@ -0,0 +1,1029 @@ +package generic + +import ( + "bytes" + "context" + "fmt" + "os" + "path/filepath" + "sort" + "strings" + "testing" + "time" + + "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/internal/detector/dbconn" + "github.com/HodeTech/leakwatch/internal/detector/shopify" + "github.com/HodeTech/leakwatch/internal/detector/testutil" + twiliodetector "github.com/HodeTech/leakwatch/internal/detector/twilio" + "github.com/HodeTech/leakwatch/internal/engine" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestStructuredConfigDetector_Metadata(t *testing.T) { + d := &StructuredConfigDetector{} + assert.Equal(t, "structured-config-secret", d.ID()) + assert.Equal(t, "Structured Configuration Secret", d.Description()) + assert.Empty(t, d.Keywords(), "escaped JSON keys require an always-run bounded parser") +} + +func TestStructuredConfigDetector_AppsettingsFixture(t *testing.T) { + data := readSyntheticAppsettings(t) + d := &StructuredConfigDetector{} + + direct := d.Scan(context.Background(), data) + viaMatcher := testutil.ScanViaMatcher(d, data) + require.Equal(t, direct, viaMatcher) + require.Len(t, direct, 4) + + wantPaths := []string{ + "XHCPClientCredentials.ClientSecret", + "BTradePrivateApiCredentials.Password", + "GtpSettings.Password", + "ElasticApm.SecretToken", + } + gotPaths := make([]string, 0, len(direct)) + for _, got := range direct { + gotPaths = append(gotPaths, got.ExtraData["key_path"]) + assert.Equal(t, "json", got.ExtraData["config_format"]) + assert.Equal(t, string(got.Raw), string(data[got.ByteStart:got.ByteEnd])) + assert.NotEqual(t, string(got.Raw), got.Redacted) + } + assert.Equal(t, wantPaths, gotPaths) +} + +func TestStructuredConfigDetector_AllSupportedFormats_MatcherAndEngineParity(t *testing.T) { + secret := "fixture-secret-9mN2pQ7rT4vW8xY" + tests := []struct { + name string + format string + path string + data []byte + valueStart string + }{ + { + name: "nested JSON", format: "json", path: "Service.Credentials.ClientSecret", + data: []byte(`{"Service":{"Credentials":{"ClientSecret":"` + secret + `"}}}`), + valueStart: secret, + }, + { + name: "nested YAML", format: "yaml", path: "service.credentials.client_secret", + data: []byte("service:\n credentials:\n client_secret: \"" + secret + "\"\n"), + valueStart: secret, + }, + { + name: "flat YAML document", format: "yaml", path: "password", + data: []byte("service: fixture\npassword: " + secret + "\n"), + valueStart: secret, + }, + { + name: "YAML literal block", format: "yaml", path: "password", + data: []byte("---\npassword: |\n " + secret + "\n"), + valueStart: secret, + }, + { + name: "YAML escaped single quote", format: "yaml", path: "service.client_secret", + data: []byte("service:\n client_secret: 'fixture-it''s-secret-9mN2pQ7r'\n"), + valueStart: "fixture-it''s-secret-9mN2pQ7r", + }, + { + name: "nested TOML", format: "toml", path: "service.credentials.client_secret", + data: []byte("[service.credentials]\nclient_secret = \"" + secret + "\"\n"), + valueStart: secret, + }, + { + name: "TOML unicode escape", format: "toml", path: "service.client_secret", + data: []byte("[service]\nclient_secret = \"fixture\\u002Dsecret-9mN2pQ7r\"\n"), + valueStart: `fixture\u002Dsecret-9mN2pQ7r`, + }, + { + name: "TOML dotted key", format: "toml", path: "service.password", + data: []byte("service.password = \"" + secret + "\"\nother = \"fixture\"\n"), + valueStart: secret, + }, + { + name: "sectionless TOML", format: "toml", path: "client_secret", + data: []byte("service_name = \"fixture\"\nclient_secret = \"" + secret + "\"\n"), + valueStart: secret, + }, + { + name: "nested XML", format: "xml", path: "configuration.service.credentials.ClientSecret", + data: []byte("" + secret + ""), + valueStart: secret, + }, + { + name: "XML CDATA", format: "xml", path: "configuration.Password", + data: []byte(""), + valueStart: secret, + }, + { + name: "dotenv", format: "dotenv", path: "CLIENT_SECRET", + data: []byte("SERVICE_NAME=fixture\nCLIENT_SECRET=" + secret + "\n"), + valueStart: secret, + }, + { + name: "YAML sequence mapping", format: "yaml", path: "users.password", + data: []byte("users:\n - password: " + secret + "\n"), + valueStart: secret, + }, + { + name: "XML key value attributes", format: "xml", path: "configuration.add.Password", + data: []byte(""), + valueStart: secret, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + det := &StructuredConfigDetector{} + direct := det.Scan(t.Context(), tc.data) + viaMatcher := testutil.ScanViaMatcher(det, tc.data) + require.Equal(t, direct, viaMatcher) + require.Len(t, direct, 1) + assert.Equal(t, tc.format, direct[0].ExtraData["config_format"]) + assert.Equal(t, tc.path, direct[0].ExtraData["key_path"]) + assert.Equal(t, tc.valueStart, string(direct[0].Raw)) + assert.Equal(t, string(direct[0].Raw), string(tc.data[direct[0].ByteStart:direct[0].ByteEnd])) + + eng := engine.New(engine.Config{Concurrency: 1, Detectors: []detector.Detector{det}}) + result, err := eng.Scan(t.Context(), &fixtureSource{data: tc.data}) + require.NoError(t, err) + require.Len(t, result.Findings, 1) + assert.Equal(t, det.ID(), result.Findings[0].DetectorID) + assert.Empty(t, result.Findings[0].Raw) + }) + } +} + +func TestStructuredConfigDetector_AllSupportedFormats_HardNegatives(t *testing.T) { + tests := map[string][]byte{ + "json": []byte(`{"Nested":{"Password":"${DATABASE_PASSWORD}","PasswordResetUrl":"https://accounts.invalid/reset/password","ClientId":"client-identifier-123456"}}`), + "yaml": []byte("nested:\n password: ${DATABASE_PASSWORD}\n password_reset_url: https://accounts.invalid/reset/password\n client_id: client-identifier-123456\n"), + "toml": []byte("[nested]\npassword = \"${DATABASE_PASSWORD}\"\npassword_reset_url = \"https://accounts.invalid/reset/password\"\nclient_id = \"client-identifier-123456\"\n"), + "xml": []byte("${DATABASE_PASSWORD}https://accounts.invalid/reset/passwordclient-identifier-123456"), + "dotenv": []byte("PASSWORD=${DATABASE_PASSWORD}\nPASSWORD_RESET_URL=https://accounts.invalid/reset/password\nCLIENT_ID=client-identifier-123456\n"), + } + for name, data := range tests { + t.Run(name, func(t *testing.T) { + assert.Empty(t, (&StructuredConfigDetector{}).Scan(t.Context(), data)) + }) + } +} + +func TestStructuredConfigDetector_CommonNamespacedCredentialKeys(t *testing.T) { + secret := "Q7mN2pL9rT4vW8xYzC6bH3kF" + tests := []struct { + name string + data []byte + want int + }{ + {name: "dotenv DB_PASSWORD after empty placeholder", data: []byte("EMPTY_VAR=\nDB_PASSWORD=" + secret + "\n"), want: 1}, + {name: "dotenv lowercase neutral line", data: []byte("optional_setting=fixture\nDATABASE_PASSWORD=" + secret + "\n"), want: 1}, + {name: "docker compose POSTGRES_PASSWORD", data: []byte("services:\n db:\n environment:\n POSTGRES_PASSWORD: " + secret + "\n"), want: 1}, + {name: "JSON camel and namespace keys", data: []byte(`{"dbPassword":"` + secret + `","AzureClientSecret":"` + secret + `","JWT_SECRET":"` + secret + `","token":"` + secret + `"}`), want: 4}, + {name: "weak trailing token remains excluded", data: []byte(`{"price-token":"` + secret + `"}`), want: 0}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + findings := (&StructuredConfigDetector{}).Scan(t.Context(), tc.data) + require.Len(t, findings, tc.want) + for _, got := range findings { + assert.Equal(t, string(got.Raw), string(tc.data[got.ByteStart:got.ByteEnd])) + } + }) + } +} + +func TestStructuredConfigDetector_RejectsLocaleLabelsAndMarkdownProse(t *testing.T) { + tests := map[string][]byte{ + "locale JSON": []byte(`{"password":"Mot de passe","secret":"Administrator credential"}`), + "Markdown prose": []byte("Password: Ask your administrator for the value\nNote: Keep this document current\n"), + "alphabetic placeholder": []byte(`{"password":"Motdepasse"}`), + "JavaScript object": []byte("const defaults = {\n username: null,\n password: null,\n};\n"), + "Rust type definition": []byte("struct CreateUser {\n email: String,\n password: String,\n}\n"), + "Python type annotation": []byte("class DbSettings(BaseModel):\n user: str\n passwd: str | None = None\n"), + "package version": []byte(`{"dependencies":{"pbkdf2-password":"1.2.1"}}`), + "package range": []byte(`{"dependencies":{"pbkdf2-password":"^1.2.1"}}`), + "common pass placeholder": []byte("host: demo-db\npassword: pass\n"), + } + for name, data := range tests { + t.Run(name, func(t *testing.T) { + assert.Empty(t, (&StructuredConfigDetector{}).Scan(t.Context(), data)) + }) + } +} + +func TestStructuredConfigDetector_XMLTruncationPreservesEarlierFindings(t *testing.T) { + secret := "Q7mN2pL9rT4vW8xYzC6bH3kF" + var data strings.Builder + data.WriteString("" + secret + "") + data.WriteString(strings.Repeat("", maxStructuredDepth+1)) + data.WriteString(strings.Repeat("", maxStructuredDepth+1)) + data.WriteString("") + + findings := (&StructuredConfigDetector{}).scanXML(t.Context(), []byte(data.String())) + require.Len(t, findings, 1) + assert.Equal(t, secret, string(findings[0].Raw)) +} + +func TestStructuredConfigDetector_YAMLAliasIsReferenceNotSecret(t *testing.T) { + data := []byte("---\nvalue: &db_password fixture-secret-9mN2pQ7r\npassword: *db_password\n") + assert.Empty(t, (&StructuredConfigDetector{}).Scan(t.Context(), data)) +} + +func TestStructuredConfigDetector_FormatAdapterBoundaries(t *testing.T) { + t.Run("XML entity preserves exact source span", func(t *testing.T) { + data := []byte(`fixture&secret-9mN2pQ7r`) + findings := (&StructuredConfigDetector{}).Scan(t.Context(), data) + require.Len(t, findings, 1) + assert.Equal(t, `fixture&secret-9mN2pQ7r`, string(findings[0].Raw)) + assert.Equal(t, string(findings[0].Raw), string(data[findings[0].ByteStart:findings[0].ByteEnd])) + }) + + t.Run("UTF-8 BOM preserves original byte span", func(t *testing.T) { + for _, data := range [][]byte{ + append([]byte{0xef, 0xbb, 0xbf}, []byte(`{"Password":"fixture-bom-secret-9mN2pQ7r"}`)...), + append([]byte{0xef, 0xbb, 0xbf}, []byte(`fixture-bom-secret-9mN2pQ7r`)...), + } { + findings := (&StructuredConfigDetector{}).Scan(t.Context(), data) + require.Len(t, findings, 1) + assert.Equal(t, "fixture-bom-secret-9mN2pQ7r", string(findings[0].Raw)) + assert.Equal(t, string(findings[0].Raw), string(data[findings[0].ByteStart:findings[0].ByteEnd])) + } + }) + + t.Run("JSON top-level array is not TOML", func(t *testing.T) { + data := []byte(`[{"Password":"fixture-array-secret-9mN2pQ7r"}]`) + findings := (&StructuredConfigDetector{}).Scan(t.Context(), data) + require.Len(t, findings, 1) + assert.Equal(t, "json", findings[0].ExtraData["config_format"]) + assert.Equal(t, "[0].Password", findings[0].ExtraData["key_path"]) + }) + + t.Run("CRLF and inline comments keep exact value span", func(t *testing.T) { + data := []byte("service:\r\n password: fixture-crlf-secret-9mN2pQ7r # explanation\r\n") + findings := (&StructuredConfigDetector{}).Scan(t.Context(), data) + require.Len(t, findings, 1) + assert.Equal(t, "fixture-crlf-secret-9mN2pQ7r", string(findings[0].Raw)) + assert.Equal(t, "service.password", findings[0].ExtraData["key_path"]) + }) + + t.Run("malformed XML returns no partial findings", func(t *testing.T) { + data := []byte(`fixture-secret-9mN2pQ7r`) + assert.Empty(t, (&StructuredConfigDetector{}).Scan(t.Context(), data)) + }) + + t.Run("large accepted line input is scanned and cancellation is prompt", func(t *testing.T) { + line := append([]byte("PASSWORD=fixture-secret-9mN2pQ7r\n#"), []byte(strings.Repeat("x", 4*1024*1024))...) + findings := (&StructuredConfigDetector{}).Scan(t.Context(), line) + require.Len(t, findings, 1) + + ctx, cancel := context.WithCancel(t.Context()) + cancel() + started := time.Now() + assert.Empty(t, (&StructuredConfigDetector{}).Scan(ctx, line)) + assert.Less(t, time.Since(started), 250*time.Millisecond) + }) + + t.Run("source code assignments are not configuration", func(t *testing.T) { + inputs := [][]byte{ + []byte("password := request.Password\nif password == \"\" { return err }\n"), + []byte("Password = GetPasswordFromRequest();\nreturn Password;\n"), + []byte("const PASSWORD = process.env.PASSWORD;\nconsole.log('configured');\n"), + []byte("PASSWORD=os.getenv(\"PASSWORD\")\n"), + } + for _, input := range inputs { + assert.Empty(t, (&StructuredConfigDetector{}).Scan(t.Context(), input), string(input)) + } + }) +} + +func TestStructuredConfigDetector_DynamicPathMetadataIsBoundedAndSafe(t *testing.T) { + secretParent := "sk-" + strings.Repeat("Ab7x", 40) + value := "fixture-value-secret-9mN2pQ7r" + data := []byte(fmt.Sprintf(`{%q:{"Password":%q}}`, secretParent, value)) + findings := (&StructuredConfigDetector{}).Scan(t.Context(), data) + require.Len(t, findings, 1) + assert.Equal(t, ".Password", findings[0].ExtraData["key_path"]) + assert.NotContains(t, findings[0].ExtraData["key_path"], secretParent) + assert.NotContains(t, findings[0].ExtraData["key_path"], value) + + controlParent := "line\nbreak" + data = []byte(fmt.Sprintf(`{%q:{"Password":%q}}`, controlParent, value)) + findings = (&StructuredConfigDetector{}).Scan(t.Context(), data) + require.Len(t, findings, 1) + assert.Equal(t, ".Password", findings[0].ExtraData["key_path"]) + assert.NotContains(t, findings[0].ExtraData["key_path"], "\n") +} + +func TestStructuredConfigDetector_LineSyntaxEdges(t *testing.T) { + t.Run("format confidence", func(t *testing.T) { + format, ok := detectLineConfigFormat(t.Context(), []byte("# comment\nexport CLIENT_SECRET=fixture-secret-9mN2pQ7r\n")) + assert.True(t, ok) + assert.Equal(t, formatDotenv, format) + + format, ok = detectLineConfigFormat(t.Context(), []byte("---\npassword: fixture-secret-9mN2pQ7r\n")) + assert.True(t, ok) + assert.Equal(t, formatYAML, format) + + for _, data := range [][]byte{ + {}, + []byte("# comment only\n"), []byte("password=value\n"), + []byte("CLIENT-SECRET=value\n"), []byte("CLIENT_SECRET=\n"), + []byte("CLIENT_SECRET=value\nnot config\n"), + } { + _, confident := detectLineConfigFormat(t.Context(), data) + assert.False(t, confident, string(data)) + } + }) + + t.Run("assignment and scalar rejection", func(t *testing.T) { + for _, line := range [][]byte{ + {}, + []byte("# comment"), []byte("; comment"), []byte("// comment"), + []byte("missing delimiter"), []byte("bad key!=value"), + } { + _, ok := parseLineAssignment(line, 0, formatDotenv) + assert.False(t, ok, string(line)) + } + + parent, ok := parseLineAssignment([]byte("\tcredentials:"), 0, formatYAML) + assert.True(t, ok) + assert.False(t, parent.hasValue) + assert.Equal(t, 8, parent.indent) + + exportLine := []byte("export CLIENT_SECRET='fixture-secret-9mN2pQ7r'") + exported, ok := parseLineAssignment(exportLine, 10, formatDotenv) + assert.True(t, ok) + assert.Equal(t, "fixture-secret-9mN2pQ7r", exported.value) + assert.Equal(t, exported.value, string(exportLine[exported.valueStart-10:exported.valueEnd-10])) + + for _, value := range [][]byte{ + {}, + []byte(" "), []byte("|"), []byte("> folded"), []byte("[array]"), []byte("{object}"), + []byte("\"unterminated"), []byte("\"valid\" trailing"), []byte("\"bad\\q\""), + } { + _, _, _, scalarOK := parseLineScalar(value, 0, formatYAML) + assert.False(t, scalarOK, string(value)) + } + _, _, _, scalarOK := parseLineScalar([]byte("bare-toml-value"), 0, formatTOML) + assert.False(t, scalarOK) + decoded, start, end, scalarOK := parseLineScalar([]byte(" \"fixture\\nsecret\" # note"), 20, formatTOML) + assert.True(t, scalarOK) + assert.Equal(t, "fixture\nsecret", decoded) + assert.Equal(t, 23, start) + assert.Greater(t, end, start) + decoded, _, _, scalarOK = parseLineScalar([]byte(`'fixture-it''s-secret-9mN2pQ7r'`), 0, formatYAML) + assert.True(t, scalarOK) + assert.Equal(t, "fixture-it's-secret-9mN2pQ7r", decoded) + decoded, _, _, scalarOK = parseLineScalar([]byte(`"fixture\Nsecret-9mN2pQ7r"`), 0, formatYAML) + assert.True(t, scalarOK) + assert.Equal(t, "fixture\u0085secret-9mN2pQ7r", decoded) + decoded, _, _, scalarOK = parseLineScalar([]byte(`"fixture\u002Dsecret-9mN2pQ7r"`), 0, formatTOML) + assert.True(t, scalarOK) + assert.Equal(t, "fixture-secret-9mN2pQ7r", decoded) + _, _, _, scalarOK = parseLineScalar([]byte(`"fixture\x2Dsecret-9mN2pQ7r"`), 0, formatTOML) + assert.False(t, scalarOK, "Go-only escapes must not be accepted as TOML") + _, _, _, scalarOK = parseLineScalar([]byte(`'fixture-it''s-secret-9mN2pQ7r'`), 0, formatTOML) + assert.False(t, scalarOK, "YAML doubled-quote syntax must not be accepted as TOML") + }) + + t.Run("helper boundaries", func(t *testing.T) { + assert.Equal(t, -1, findClosingQuote([]byte(`"escaped\"`), '"')) + assert.Equal(t, 10, findClosingQuote([]byte(`"escaped\\"`), '"')) + assert.False(t, isPlainConfigKey(nil)) + assert.False(t, isPlainConfigKey([]byte("bad key"))) + assert.True(t, isPlainConfigKey([]byte("service.credentials"))) + + for _, section := range [][]byte{[]byte("[]"), []byte("[[array.table]]"), []byte("[bad section]"), []byte("plain")} { + _, ok := parseTOMLSection(section) + assert.False(t, ok, string(section)) + } + parts, ok := parseTOMLSection([]byte("[ service.credentials ]")) + assert.True(t, ok) + assert.Equal(t, []string{"service", "credentials"}, parts) + parts, ok = parseTOMLSection([]byte("[service.credentials] # provider settings")) + assert.True(t, ok) + assert.Equal(t, []string{"service", "credentials"}, parts) + + parts, ok = parseTOMLDottedKey([]byte(`"service".'credentials'.password`)) + assert.True(t, ok) + assert.Equal(t, []string{"service", "credentials", "password"}, parts) + for _, key := range [][]byte{ + nil, []byte("service..password"), []byte(`"".password`), + []byte(`"unterminated.password`), []byte("bad key.password"), + } { + _, ok := parseTOMLDottedKey(key) + assert.False(t, ok, string(key)) + } + + for _, indicator := range [][]byte{ + []byte("|"), []byte(">-"), []byte("|2 # comment"), []byte(">+4"), + } { + assert.True(t, isYAMLBlockIndicator(indicator), string(indicator)) + } + for _, indicator := range [][]byte{ + nil, []byte("plain"), []byte("| trailing"), []byte(">!"), + } { + assert.False(t, isYAMLBlockIndicator(indicator), string(indicator)) + } + + assert.False(t, hasNestedYAMLSignals([]yamlSignal{{indent: 0, hasValue: true}})) + assert.False(t, hasNestedYAMLSignals([]yamlSignal{{indent: 0}, {indent: 0, hasValue: true}})) + assert.True(t, hasNestedYAMLSignals([]yamlSignal{{indent: 0}, {indent: 2, hasValue: true}})) + }) + + t.Run("YAML block scalar decoding and failure boundaries", func(t *testing.T) { + literal := []byte(" first-secret-line\n second-secret-line\nsibling: value\n") + value, start, end, next, ok := parseYAMLBlockScalar(t.Context(), literal, 0, 0, '|') + assert.True(t, ok) + assert.Equal(t, "first-secret-line\nsecond-secret-line", value) + assert.Equal(t, "first-secret-line\n second-secret-line", string(literal[start:end])) + assert.Equal(t, strings.Index(string(literal), "sibling:"), next) + + folded := []byte(" first-secret-line\n\n second-secret-line\n") + value, _, _, _, ok = parseYAMLBlockScalar(t.Context(), folded, 0, 0, '>') + assert.True(t, ok) + assert.Equal(t, "first-secret-line\nsecond-secret-line", value) + + ctx, cancel := context.WithCancel(t.Context()) + cancel() + _, _, _, _, ok = parseYAMLBlockScalar(ctx, literal, 0, 0, '|') + assert.False(t, ok) + for _, invalid := range [][]byte{ + []byte("\tsecret\n"), []byte("secret\n"), []byte(" \n"), + } { + _, _, _, _, ok = parseYAMLBlockScalar(t.Context(), invalid, 0, 0, '|') + assert.False(t, ok, string(invalid)) + } + }) + + t.Run("YAML sibling paths and cancellation", func(t *testing.T) { + data := []byte("first:\n password: fixture-first-secret-9mN2pQ7r\nsecond:\n client_secret: fixture-second-secret-9mN2pQ7r\n") + findings := (&StructuredConfigDetector{}).Scan(t.Context(), data) + require.Len(t, findings, 2) + assert.Equal(t, "first.password", findings[0].ExtraData["key_path"]) + assert.Equal(t, "second.client_secret", findings[1].ExtraData["key_path"]) + + ctx, cancel := context.WithCancel(t.Context()) + cancel() + assert.Empty(t, (&StructuredConfigDetector{}).Scan(ctx, []byte("CLIENT_SECRET=fixture-secret-9mN2pQ7r\n"))) + assert.Empty(t, (&StructuredConfigDetector{}).scanLineConfig(t.Context(), []byte{0xff, 0xfe})) + }) +} + +func TestStructuredConfigDetector_XMLSyntaxEdges(t *testing.T) { + det := &StructuredConfigDetector{} + + t.Run("whitespace source span", func(t *testing.T) { + data := []byte("outside \t\r\nfixture-secret-9mN2pQ7r \n") + findings := det.Scan(t.Context(), data) + require.Len(t, findings, 1) + assert.Equal(t, "fixture-secret-9mN2pQ7r", string(findings[0].Raw)) + }) + + t.Run("comments and child elements invalidate scalar", func(t *testing.T) { + inputs := [][]byte{ + []byte(`fixture-secret-9mN2pQ7r`), + []byte(`fixture-secret-9mN2pQ7r`), + } + for _, input := range inputs { + assert.Empty(t, det.Scan(t.Context(), input)) + } + }) + + t.Run("deep indented XML remains safe through detector dispatch", func(t *testing.T) { + for _, depth := range []int{16, maxStructuredDepth - 1} { + t.Run(fmt.Sprintf("depth-%d", depth), func(t *testing.T) { + var input strings.Builder + for i := range depth { + fmt.Fprintf(&input, "\n", i) + } + input.WriteString("fixture-secret-9mN2pQ7r\n") + for i := depth - 1; i >= 0; i-- { + fmt.Fprintf(&input, "\n", i) + } + data := []byte(input.String()) + + var findings []detector.RawFinding + assert.NotPanics(t, func() { + findings = det.Scan(t.Context(), data) + }) + require.Len(t, findings, 1) + assert.Equal(t, "fixture-secret-9mN2pQ7r", string(findings[0].Raw)) + wantStart := bytes.Index(data, findings[0].Raw) + assert.Equal(t, wantStart, findings[0].ByteStart) + assert.Equal(t, wantStart+len(findings[0].Raw), findings[0].ByteEnd) + }) + } + }) + + t.Run("invalid input depth and cancellation fail closed", func(t *testing.T) { + assert.Empty(t, det.scanXML(t.Context(), []byte{0xff, '<'})) + deep := []byte(strings.Repeat("", maxStructuredDepth+1) + strings.Repeat("", maxStructuredDepth+1)) + assert.Empty(t, det.scanXML(t.Context(), deep)) + ctx, cancel := context.WithCancel(t.Context()) + cancel() + assert.Empty(t, det.scanXML(ctx, []byte(`fixture-secret-9mN2pQ7r`))) + }) +} + +func TestStructuredConfigDetector_AppsettingsFullEngine_EightLocationsSixValues(t *testing.T) { + data := readSyntheticAppsettings(t) + detectors := []detector.Detector{ + &dbconn.ConnectionString{}, + &APIKeyDetector{}, + &StructuredConfigDetector{}, + } + eng := engine.New(engine.Config{ + Concurrency: 3, + Detectors: detectors, + EnableEntropy: true, + EntropyThreshold: 4.0, + }) + + result, err := eng.Scan(context.Background(), &fixtureSource{data: data}) + require.NoError(t, err) + require.Len(t, result.Findings, 8, "fixture must produce one finding per secret-bearing location without overlap duplicates") + + lines := make([]int, 0, len(result.Findings)) + counts := make(map[string]int) + for _, got := range result.Findings { + lines = append(lines, got.SourceMetadata.Line) + counts[got.DetectorID]++ + assert.Empty(t, got.Raw, "default engine output must not expose fixture values") + } + assert.Equal(t, []int{3, 7, 11, 31, 37, 43, 49, 52}, lines) + assert.Equal(t, map[string]int{ + "database-connection-string": 1, + "generic-api-key": 3, + "structured-config-secret": 4, + }, counts) + + unique := make(map[string]struct{}) + for _, det := range detectors { + for _, raw := range det.Scan(context.Background(), data) { + unique[string(raw.Raw)] = struct{}{} + } + } + assert.Len(t, unique, 6, "three APISIX locations intentionally share one synthetic value") +} + +func TestStructuredConfigDetector_PairedTwilioSecretUsesProviderFindingOnly(t *testing.T) { + keySID := "SK" + strings.Repeat("ab12cd34", 4) + secret := strings.Repeat("Qw12Er34", 4) + data := []byte(`{"Twilio":{"ApiKeySid":"` + keySID + `","ApiKeySecret":"` + secret + `"}}`) + eng := engine.New(engine.Config{ + Concurrency: 2, + Detectors: []detector.Detector{ + &StructuredConfigDetector{}, + &twiliodetector.Detector{}, + }, + }) + + result, err := eng.Scan(context.Background(), &fixtureSource{data: data}) + require.NoError(t, err) + require.Len(t, result.Findings, 1) + assert.Equal(t, "twilio-api-key", result.Findings[0].DetectorID) + assert.Empty(t, result.Findings[0].Raw) +} + +func TestStructuredConfigDetector_HardNegatives(t *testing.T) { + data := []byte(`{ + "ClientId": "client-identifier-123456", + "Username": "service-account-name", + "PublicKey": "public-key-identifier", + "PasswordHash": "9mN2pQ7rT4vW8xY0zA1bC5dF", + "PasswordResetUrl": "https://accounts.invalid/reset/password", + "primary-price-token": "market-price-feed-name", + "CacheKey": "http-audit-log-cache-key-v1", + "Formatter": "Serilog.Formatting.Compact.CompactJsonFormatter", + "expression": "RequestPath not like '/health%' and StatusCode >= 500", + "SensitiveHeaders": ["Authorization", "Password", "X-APISIX-KEY"], + "EmptyPassword": "", + "Nested": { + "Password": "${DATABASE_PASSWORD}", + "ClientSecret": "{{ secret_ref }}", + "SecretToken": "%ELASTIC_APM_TOKEN%", + "AccessToken": "vault://team/service/token", + "RefreshToken": "@Microsoft.KeyVault(SecretUri=https://vault.invalid/secrets/token)", + "SigningSecret": "APISIX_ADMIN_KEY", + "WebhookSecret": "0000000000000000", + "ConsumerSecret": "change_me", + "Password": "string", + "Passphrase": "redacted", + "AuthToken": "$AUTH_TOKEN", + "AuthToken": "$auth_token", + "BearerToken": "$(BEARER_TOKEN)", + "AppSecret": "", + "ClientSecret": "/run/secrets/client-secret", + "Password": "file:///run/secrets/db-password", + "PrivateKey": "/etc/ssl/private/key.pem", + "PrivateKey": "certs/private-key.pem", + "PrivateKey": "C:\\certs\\private-key.pem", + "PrivateKey": "\\\\server\\share\\private-key.pem", + "MasterKey": "pkcs11:token=application;object=master-key", + "EncryptionKey": "arn:aws:kms:eu-west-1:123456789012:key/example", + "ApplicationSecret": "op://engineering/service/credential" + } +}`) + + findings := (&StructuredConfigDetector{}).Scan(context.Background(), data) + assert.Empty(t, findings) +} + +func TestStructuredConfigDetector_JSONCCommentsTrailingCommaAndRecovery(t *testing.T) { + data := []byte(`{ + // "Password": "comment-only-secret-9mN2pQ7r" + "BrokenScalar": true + "Nested": { + /* "ClientSecret": "block-comment-secret-9mN2pQ7r" */ + "ClientSecret": "actual-fixture-secret-9mN2pQ7r", + }, +}`) + + findings := (&StructuredConfigDetector{}).Scan(context.Background(), data) + require.Len(t, findings, 1, "missing comma recovery must not hide a later valid key/value pair") + assert.Equal(t, "Nested.ClientSecret", findings[0].ExtraData["key_path"]) +} + +func TestStructuredConfigDetector_EscapedKeyAndExactSourceSpan(t *testing.T) { + data := []byte(`{"Pass\u0077ord":"ab\\cd-9mN2pQ7r"}`) + findings := (&StructuredConfigDetector{}).Scan(context.Background(), data) + require.Len(t, findings, 1) + assert.Equal(t, "Password", findings[0].ExtraData["key_name"]) + assert.Equal(t, `ab\\cd-9mN2pQ7r`, string(findings[0].Raw), "Raw must be the exact source representation, not a whole line") + assert.Equal(t, string(findings[0].Raw), string(data[findings[0].ByteStart:findings[0].ByteEnd])) +} + +func TestStructuredConfigDetector_StrictJSONEscapes(t *testing.T) { + tests := []struct { + name string + data []byte + }{ + {name: "escaped slash", data: []byte(`{"Password":"ab\/cd-secret"}`)}, + {name: "BMP unicode", data: []byte(`{"Password":"secret-\u00e7"}`)}, + {name: "surrogate pair", data: []byte(`{"Password":"secret-\uD83D\uDE00"}`)}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + findings := (&StructuredConfigDetector{}).Scan(context.Background(), tc.data) + require.Len(t, findings, 1) + assert.Equal(t, string(findings[0].Raw), string(tc.data[findings[0].ByteStart:findings[0].ByteEnd])) + }) + } + + invalid := [][]byte{ + []byte(`{"Password":"secret-\x41"}`), + []byte(`{"Password":"secret-\a"}`), + []byte(`{"Password":"secret-\uD83D"}`), + []byte(`{"Password":"secret-\uDE00"}`), + []byte(`{"Password":"secret-\uD83D\u0041"}`), + } + for _, data := range invalid { + assert.Empty(t, (&StructuredConfigDetector{}).Scan(context.Background(), data)) + } +} + +func TestStructuredConfigDetector_SlashBearingSecretIsNotMistakenForPath(t *testing.T) { + data := []byte(`{"ClientSecret":"abc/def+ghi=9mN2"}`) + require.Len(t, (&StructuredConfigDetector{}).Scan(context.Background(), data), 1) +} + +func TestStructuredConfigDetector_KeyNormalization(t *testing.T) { + tests := map[string]string{ + "ClientSecret": "clientsecret", + "client_secret": "clientsecret", + "client-secret": "clientsecret", + "CLIENT_SECRET": "clientsecret", + "SecretToken": "secrettoken", + "signing.secret": "signingsecret", + "price-token": "pricetoken", + } + for input, want := range tests { + t.Run(input, func(t *testing.T) { + assert.Equal(t, want, canonicalConfigKey(input)) + }) + } +} + +func TestStructuredConfigDetector_SupportedSecretRoles(t *testing.T) { + keys := []string{ + "Password", "Passwd", "Passphrase", "Secret", "ClientSecret", + "SecretToken", "AccessToken", "RefreshToken", "AuthToken", "BearerToken", + "SigningSecret", "WebhookSecret", "ConsumerSecret", "AppSecret", + "ApplicationSecret", "MasterSecret", "MasterKey", "EncryptionKey", "PrivateKey", + } + var b strings.Builder + b.WriteByte('{') + for i, key := range keys { + if i > 0 { + b.WriteByte(',') + } + fmt.Fprintf(&b, "%q:%q", key, fmt.Sprintf("fixture-%02d-9mN2pQ7rT4vW8xY", i)) + } + b.WriteByte('}') + + findings := (&StructuredConfigDetector{}).Scan(context.Background(), []byte(b.String())) + require.Len(t, findings, len(keys)) + for i, got := range findings { + assert.Equal(t, keys[i], got.ExtraData["key_name"]) + } +} + +func TestStructuredConfigDetector_EverySupportedRoleMatchesThroughMatcher(t *testing.T) { + keys := []string{ + "Password", "Passwd", "Passphrase", "Secret", "ClientSecret", + "SecretToken", "AccessToken", "RefreshToken", "AuthToken", "BearerToken", + "SigningSecret", "WebhookSecret", "ConsumerSecret", "AppSecret", + "ApplicationSecret", "MasterSecret", "MasterKey", "EncryptionKey", "PrivateKey", + } + d := &StructuredConfigDetector{} + for i, key := range keys { + t.Run(key, func(t *testing.T) { + data := []byte(fmt.Sprintf("{%q:%q}", key, fmt.Sprintf("fixture-%02d-9mN2pQ7rT4vW8xY", i))) + assert.Equal(t, d.Scan(context.Background(), data), testutil.ScanViaMatcher(d, data)) + require.Len(t, testutil.ScanViaMatcher(d, data), 1) + }) + } + + escaped := []byte(`{"Pass\u0077ord":"fixture-escaped-9mN2pQ7r"}`) + assert.Equal(t, d.Scan(context.Background(), escaped), testutil.ScanViaMatcher(d, escaped)) + require.Len(t, testutil.ScanViaMatcher(d, escaped), 1) +} + +func TestStructuredConfigDetector_ProviderOverlapPrefersSpecializedFinding(t *testing.T) { + data := []byte(`{"AccessToken":"shpat_0123456789abcdef0123456789abcdef"}`) + eng := engine.New(engine.Config{ + Concurrency: 1, + Detectors: []detector.Detector{ + &StructuredConfigDetector{}, + &shopify.Detector{}, + }, + }) + + result, err := eng.Scan(context.Background(), &fixtureSource{data: data}) + require.NoError(t, err) + require.Len(t, result.Findings, 1) + assert.Equal(t, "shopify-access-token", result.Findings[0].DetectorID) +} + +func TestProviderDetectors_ExactSpansPreventRejectedDuplicateInlineIgnore(t *testing.T) { + t.Run("Shopify", func(t *testing.T) { + token := "shpat_0123456789abcdef0123456789abcdef" + data := []byte("prefix_" + token + " # leakwatch:ignore:shopify-access-token\n" + + "SHOPIFY_ACCESS_TOKEN=" + token + "\n") + eng := engine.New(engine.Config{Concurrency: 1, Detectors: []detector.Detector{&shopify.Detector{}}}) + + result, err := eng.Scan(context.Background(), &fixtureSource{data: data}) + require.NoError(t, err) + require.Len(t, result.Findings, 1) + assert.Equal(t, 2, result.Findings[0].SourceMetadata.Line) + }) + + t.Run("Twilio", func(t *testing.T) { + secret := strings.Repeat("Qw12Er34", 4) + keySID := "SK" + strings.Repeat("ab12cd34", 4) + data := []byte("NOTE=" + secret + " # leakwatch:ignore:twilio-api-key\n" + + "TWILIO_API_KEY_SID=" + keySID + "\n" + + "TWILIO_API_KEY_SECRET=" + secret + "\n") + eng := engine.New(engine.Config{Concurrency: 1, Detectors: []detector.Detector{&twiliodetector.Detector{}}}) + + result, err := eng.Scan(context.Background(), &fixtureSource{data: data}) + require.NoError(t, err) + require.Len(t, result.Findings, 1) + assert.Equal(t, 3, result.Findings[0].SourceMetadata.Line) + }) + + t.Run("Twilio rejects template values and malformed SID context", func(t *testing.T) { + keySID := "SK" + strings.Repeat("ab12cd34", 4) + secret := strings.Repeat("Qw12Er34", 4) + for _, data := range [][]byte{ + []byte("TWILIO_API_KEY_SID=" + keySID + "\nTWILIO_API_KEY_SECRET=YOUR_TWILIO_API_KEY_SECRET\n"), + []byte("TWILIO_API_KEY_SID=" + keySID + "-suffix\nTWILIO_API_KEY_SECRET=" + secret + "\n"), + } { + eng := engine.New(engine.Config{Concurrency: 1, Detectors: []detector.Detector{&twiliodetector.Detector{}}}) + result, err := eng.Scan(context.Background(), &fixtureSource{data: data}) + require.NoError(t, err) + assert.Empty(t, result.Findings) + } + }) +} + +func TestStructuredConfigDetector_ResourceBounds(t *testing.T) { + t.Run("depth", func(t *testing.T) { + data := []byte(strings.Repeat(`{"nested":`, maxStructuredDepth+2) + + `{"Password":"fixture-secret-9mN2pQ7r"}` + strings.Repeat("}", maxStructuredDepth+3)) + assert.Empty(t, (&StructuredConfigDetector{}).Scan(context.Background(), data)) + }) + + t.Run("deep non-empty array terminates", func(t *testing.T) { + data := []byte(`{"Deep":` + strings.Repeat("[", maxStructuredDepth+2) + + `"ordinary-value"` + strings.Repeat("]", maxStructuredDepth+2) + + `,"Password":"shallow-fixture-secret-9mN2pQ7r"}`) + done := make(chan []detector.RawFinding, 1) + go func() { + done <- (&StructuredConfigDetector{}).Scan(context.Background(), data) + }() + select { + case findings := <-done: + require.Len(t, findings, 1) + assert.Equal(t, "Password", findings[0].ExtraData["key_name"]) + case <-time.After(time.Second): + t.Fatal("deep array scan did not terminate") + } + }) + + t.Run("finding cap", func(t *testing.T) { + var b strings.Builder + b.WriteByte('{') + for i := 0; i < maxStructuredFindings+20; i++ { + if i > 0 { + b.WriteByte(',') + } + fmt.Fprintf(&b, "%q:%q", "Password", fmt.Sprintf("fixture-%04d-9mN2pQ7r", i)) + } + b.WriteByte('}') + assert.Len(t, (&StructuredConfigDetector{}).Scan(context.Background(), []byte(b.String())), maxStructuredFindings) + }) +} + +func TestStructuredConfigDetector_TokenRetentionCapCannotHideTrailingSecret(t *testing.T) { + var b strings.Builder + b.Grow(maxStructuredTokens + 128) + b.WriteByte('[') + for i := 0; i < maxStructuredTokens/2+10; i++ { + b.WriteString("0,") + } + b.WriteString(`{"Password":"trailing-fixture-secret-9mN2pQ7r"}]`) + + findings := (&StructuredConfigDetector{}).Scan(context.Background(), []byte(b.String())) + require.Len(t, findings, 1) + assert.Equal(t, "Password", findings[0].ExtraData["key_name"]) + assert.Equal(t, "Password", findings[0].ExtraData["key_path"], "path safely falls back after retention cap") +} + +func TestStructuredConfigDetector_CancelledContextReturnsNoPartialResults(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + data := []byte(`{"Password":"fixture-secret-9mN2pQ7r"}`) + assert.Empty(t, (&StructuredConfigDetector{}).Scan(ctx, data)) +} + +func TestStructuredConfigDetector_MalformedAndNonJSONInputsDoNotPanic(t *testing.T) { + inputs := [][]byte{ + nil, + {}, + []byte(`{"Password":"unterminated`), + []byte(`{"Password":"bad\qescape"}`), + []byte(`/* unterminated comment`), + []byte(`Password=fixture-secret-9mN2pQ7r`), + } + for _, input := range inputs { + assert.NotPanics(t, func() { + _ = (&StructuredConfigDetector{}).Scan(context.Background(), input) + }) + } +} + +func TestStructuredConfigDetector_LexerAndReferenceBoundaries(t *testing.T) { + t.Run("string bounds", func(t *testing.T) { + _, next, ok, complete := scanJSONString(context.Background(), []byte(`"trailing\`), 0, 0) + assert.False(t, ok) + assert.True(t, complete) + assert.Equal(t, len(`"trailing\`), next) + + overlong := []byte(`"` + strings.Repeat("a", maxStructuredStringLen+1) + `"`) + _, next, ok, complete = scanJSONString(context.Background(), overlong, 0, 0) + assert.False(t, ok) + assert.True(t, complete) + assert.Equal(t, len(overlong), next) + }) + + t.Run("path parser boundaries", func(t *testing.T) { + p := &jsonPathParser{} + p.parseValue(nil, 0) + assert.Zero(t, p.pos) + + paths := buildJSONPaths([]jsonToken{ + {kind: jsonObjectStart}, + {kind: jsonString, text: "missing-colon"}, + {kind: jsonOther}, + {kind: jsonObjectEnd}, + }) + assert.Empty(t, paths) + }) + + t.Run("template syntax", func(t *testing.T) { + assert.False(t, isDollarReference("")) + assert.False(t, isDollarReference("$")) + assert.False(t, isDollarReference("$NOT-A-REFERENCE")) + assert.True(t, isDollarReference("$lower_case_1")) + assert.False(t, isAngleReference("<>")) + assert.False(t, isAngleReference("")) + assert.True(t, isAngleReference("")) + }) + + t.Run("value length", func(t *testing.T) { + assert.False(t, isContextSecretValue("Password", "abc")) + assert.False(t, isContextSecretValue("Password", strings.Repeat("x", maxStructuredStringLen+1))) + assert.True(t, isContextSecretValue("Password", "s3cure")) + }) +} + +type pollCancelContext struct { + done chan struct{} + polls int + closed bool +} + +func newPollCancelContext() *pollCancelContext { + return &pollCancelContext{done: make(chan struct{})} +} + +func (c *pollCancelContext) Deadline() (time.Time, bool) { return time.Time{}, false } + +func (c *pollCancelContext) Done() <-chan struct{} { + c.poll() + return c.done +} + +func (c *pollCancelContext) Err() error { + c.poll() + if c.closed { + return context.Canceled + } + return nil +} + +func (c *pollCancelContext) Value(any) any { return nil } + +func (c *pollCancelContext) poll() { + c.polls++ + if c.polls >= 2 && !c.closed { + close(c.done) + c.closed = true + } +} + +func TestStructuredConfigDetector_LongTokenLoopsHonorCancellation(t *testing.T) { + inputs := map[string][]byte{ + "string": []byte(`"` + strings.Repeat("a", 1<<20)), + "line comment": []byte(`//` + strings.Repeat("a", 1<<20)), + "block comment": []byte(`/*` + strings.Repeat("a", 1<<20)), + "bare scalar": []byte(strings.Repeat("a", 1<<20)), + } + for name, data := range inputs { + t.Run(name, func(t *testing.T) { + _, _, complete := lexJSONLike(newPollCancelContext(), data) + assert.False(t, complete) + }) + } +} + +func TestStructuredConfigDetector_DeterministicSourceOrder(t *testing.T) { + data := []byte(`{"Z":{"Password":"fixture-z-9mN2pQ7r"},"A":{"ClientSecret":"fixture-a-9mN2pQ7r"}}`) + findings := (&StructuredConfigDetector{}).Scan(context.Background(), data) + require.Len(t, findings, 2) + paths := []string{findings[0].ExtraData["key_path"], findings[1].ExtraData["key_path"]} + assert.Equal(t, []string{"Z.Password", "A.ClientSecret"}, paths) + assert.True(t, sort.IntsAreSorted([]int{findings[0].ByteStart, findings[1].ByteStart})) +} + +func FuzzStructuredConfigDetector(f *testing.F) { + f.Add([]byte(`{"Password":"fixture-secret-9mN2pQ7r"}`)) + f.Add([]byte(`{"SensitiveHeaders":["Password"]}`)) + f.Add([]byte(`{"Password":"${PASSWORD}"}`)) + f.Add([]byte(`{"Password":"ab\/cd-secret"}`)) + f.Add([]byte(`{"Password":"secret-\uD83D\uDE00"}`)) + f.Add([]byte("service:\n password: fixture-secret-9mN2pQ7r\n")) + f.Add([]byte("[service]\npassword = \"fixture-secret-9mN2pQ7r\"\n")) + f.Add([]byte(`fixture&secret-9mN2pQ7r`)) + f.Add([]byte("PASSWORD=fixture-secret-9mN2pQ7r\n")) + f.Add([]byte(strings.Repeat("[", maxStructuredDepth+2) + `"Password"` + strings.Repeat("]", maxStructuredDepth+2))) + f.Fuzz(func(t *testing.T, data []byte) { + if len(data) > 1<<20 { + t.Skip() + } + findings := (&StructuredConfigDetector{}).Scan(context.Background(), data) + for _, got := range findings { + require.GreaterOrEqual(t, got.ByteStart, 0) + require.Greater(t, got.ByteEnd, got.ByteStart) + require.LessOrEqual(t, got.ByteEnd, len(data)) + require.Equal(t, string(got.Raw), string(data[got.ByteStart:got.ByteEnd])) + } + }) +} + +func BenchmarkStructuredConfigDetector_Appsettings(b *testing.B) { + data, err := os.ReadFile(filepath.Join("testdata", "appsettings.synthetic.json")) + if err != nil { + b.Fatal(err) + } + d := &StructuredConfigDetector{} + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = d.Scan(context.Background(), data) + } +} diff --git a/internal/detector/generic/testdata/README.md b/internal/detector/generic/testdata/README.md new file mode 100644 index 0000000..42c229e --- /dev/null +++ b/internal/detector/generic/testdata/README.md @@ -0,0 +1,11 @@ +# Synthetic appsettings fixture + +`appsettings.synthetic.json` is a deterministic test fixture created from +scratch. Its values were not copied, sanitized, hashed, prefixed, suffixed, or +otherwise derived from a production or user credential. + +The file intentionally contains credential-shaped strings so detector tests can +exercise realistic JSON structure, repeated-value location handling, entropy +boundaries, and hard-negative fields. Secret-scanning exclusions are scoped to +that exact JSON file; this README and any future fixture remain scanned unless +they receive their own reviewed exception. diff --git a/internal/detector/generic/testdata/appsettings.synthetic.json b/internal/detector/generic/testdata/appsettings.synthetic.json new file mode 100644 index 0000000..6932123 --- /dev/null +++ b/internal/detector/generic/testdata/appsettings.synthetic.json @@ -0,0 +1,62 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Host=synthetic.invalid;Database=fixture;Username=fixture_user;Password=R7mN2pL9rT4vW8xY0zA1bC3dE5fG6h" + }, + "XHCPClientCredentials": { + "ClientId": "fixture-client-id-01", + "ClientSecret": "mN7pQ2rT9vW4xY8zA1bC5dF6gH3jK0lP" + }, + "BTradePrivateApiCredentials": { + "Username": "fixture-private-api-user", + "Password": "vW8xY2zA6bC1dF5gH9jK3mN7pQ4rT0sU" + }, + "DataVendorSettings": { + "primary-price-token": "", + "secondary-price-token": "", + "delayed-price-token": "", + "snapshot-price-token": "" + }, + "Serilog": { + "Filter": { + "Args": { + "expression": "RequestPath not like '/health%' and StatusCode >= 500" + } + }, + "Formatter": "Serilog.Formatting.Compact.CompactJsonFormatter, Serilog.Formatting.Compact" + }, + "ApiEndPoints": { + "BTrade": { + "BaseUrl": "https://btrade.synthetic.invalid/api", + "DefaultRequestHeaders": { + "X-APISIX-KEY": "Q7mN2pL9rT4vW8xY0zA1bC3dE5fG6hJ8kM2nP4sR7tV" + } + }, + "Asgard": { + "BaseUrl": "https://asgard.synthetic.invalid/api", + "DefaultRequestHeaders": { + "X-APISIX-KEY": "Q7mN2pL9rT4vW8xY0zA1bC3dE5fG6hJ8kM2nP4sR7tV" + } + }, + "MatriksData": { + "BaseUrl": "https://matriks.synthetic.invalid/api", + "DefaultRequestHeaders": { + "X-APISIX-KEY": "Q7mN2pL9rT4vW8xY0zA1bC3dE5fG6hJ8kM2nP4sR7tV" + } + } + }, + "GtpSettings": { + "Username": "fixture-gtp-user", + "Password": "pL9rT4vW8xY0zA1bC3dE5fG6hJ8kM2nP4sR7tV" + }, + "ElasticApm": { + "SecretToken": "R7mN2pL9rT4vW8" + }, + "HttpAuditLog": { + "SensitiveHeaders": [ + "Authorization", + "Cookie", + "X-APISIX-KEY" + ], + "CacheKey": "http-audit-log-cache-key-v1" + } +} diff --git a/internal/detector/github/github_oauth.go b/internal/detector/github/github_oauth.go index af75b06..d180ead 100644 --- a/internal/detector/github/github_oauth.go +++ b/internal/detector/github/github_oauth.go @@ -3,6 +3,7 @@ package github import ( "context" "regexp" + "strings" "github.com/HodeTech/leakwatch/internal/detector" "github.com/HodeTech/leakwatch/pkg/finding" @@ -64,10 +65,12 @@ func (d *OAuthDetector) Scan(_ context.Context, data []byte) []detector.RawFindi findings := make([]detector.RawFinding, 0, len(matches)) for _, match := range matches { + subtype := strings.TrimSuffix(string(match[:4]), "_") findings = append(findings, detector.RawFinding{ DetectorID: d.ID(), Raw: match, Redacted: detector.RedactBytes(match), + ExtraData: map[string]string{"token_subtype": subtype}, }) } return findings diff --git a/internal/detector/github/github_oauth_test.go b/internal/detector/github/github_oauth_test.go index b6474b5..822c9e0 100644 --- a/internal/detector/github/github_oauth_test.go +++ b/internal/detector/github/github_oauth_test.go @@ -97,6 +97,18 @@ func TestOAuthDetector_Scan_MatchAndReject(t *testing.T) { } } +func TestOAuthDetector_Scan_RecordsNonSecretSubtypeContext(t *testing.T) { + suffix := strings.Repeat("Abc1D678", 5) + for _, subtype := range []string{"gho", "ghu", "ghr", "ghs"} { + t.Run(subtype, func(t *testing.T) { + findings := (&OAuthDetector{}).Scan(context.Background(), []byte(subtype+"_"+suffix)) + require.Len(t, findings, 1) + assert.Equal(t, subtype, findings[0].ExtraData["token_subtype"]) + assert.NotContains(t, findings[0].ExtraData["token_subtype"], suffix) + }) + } +} + // fakeStatelessToken builds an obviously-fake GitHub stateless installation // token of the ghs_APPID_ form (header.payload.signature). It is assembled // from parts at runtime so the source file never contains a contiguous, diff --git a/internal/detector/gitlab/gitlab_token.go b/internal/detector/gitlab/gitlab_token.go index f7fac0d..6d21f2b 100644 --- a/internal/detector/gitlab/gitlab_token.go +++ b/internal/detector/gitlab/gitlab_token.go @@ -10,20 +10,15 @@ import ( "github.com/HodeTech/leakwatch/pkg/finding" ) -var ( - // gitlabTokenPattern matches the classic personal access token (glpat-) as - // well as the newer routable prefixed token families: deploy (gldt-), runner - // (glrt-), CI/CD build & trigger (glcbt-/glptt-), OAuth application secret - // (gloas-), and feed (glft-) tokens. The body quantifier is open-ended - // ({20,}) so token-length changes under an existing prefix do not silently - // drop the match. - gitlabTokenPattern = regexp.MustCompile(`(?:glpat|gldt|glrt|glcbt|glptt|gloas|glft)-[A-Za-z0-9_\-]{20,}`) - // gitlabHostPattern captures a co-located self-hosted GitLab instance host - // (any URL whose host contains "gitlab") so the verifier can target the - // token's true issuer instead of defaulting to gitlab.com. Capture group 1 - // is the bare host (with optional port). - gitlabHostPattern = regexp.MustCompile(`https?://([a-zA-Z0-9.-]*gitlab[a-zA-Z0-9.-]*(?::\d+)?)`) -) +// gitlabTokenPattern matches the classic personal access token (glpat-) as +// well as the newer routable prefixed token families: deploy (gldt-), runner +// (glrt-/glrtr-), CI/CD build & trigger (glcbt-/glptt-), incoming mail +// (glimt-), agent (glagent-), workhorse (glwt-), service-account +// (glsoat-), feature-flags client (glffct-), OAuth application secret +// (gloas-), and feed (glft-) tokens. The body quantifier is open-ended +// ({20,}) so token-length changes under an existing prefix do not silently +// drop the match. +var gitlabTokenPattern = regexp.MustCompile(`(?:glpat|gldt|glrt|glrtr|glcbt|glptt|glimt|glagent|glwt|glsoat|glffct|gloas|glft)-[A-Za-z0-9_\-]{20,}`) // Detector detects GitLab Personal Access Tokens. type Detector struct{} @@ -33,7 +28,10 @@ func (d *Detector) ID() string { return "gitlab-pat" } func (d *Detector) Description() string { return "GitLab Personal Access Token" } func (d *Detector) Keywords() []string { - return []string{"glpat-", "gldt-", "glrt-", "glcbt-", "glptt-", "gloas-", "glft-"} + return []string{ + "glpat-", "gldt-", "glrt-", "glrtr-", "glcbt-", "glptt-", + "glimt-", "glagent-", "glwt-", "glsoat-", "glffct-", "gloas-", "glft-", + } } func (d *Detector) Severity() finding.Severity { return finding.SeverityCritical } @@ -45,13 +43,6 @@ func (d *Detector) Scan(_ context.Context, data []byte) []detector.RawFinding { return nil } - // Capture a co-located self-hosted GitLab host so the verifier does not - // misreport a live self-hosted token against gitlab.com. Non-secret context. - var host string - if m := gitlabHostPattern.FindSubmatch(data); m != nil { - host = string(m[1]) - } - findings := make([]detector.RawFinding, 0, len(matches)) for _, match := range matches { // Preserve the actual prefix (up to and including the "-") in the @@ -62,9 +53,6 @@ func (d *Detector) Scan(_ context.Context, data []byte) []detector.RawFinding { Raw: bytes.Clone(match), Redacted: string(match[:prefixEnd]) + "****" + string(match[len(match)-4:]), } - if host != "" { - f.ExtraData = map[string]string{"host": host} - } findings = append(findings, f) } return findings diff --git a/internal/detector/gitlab/gitlab_token_test.go b/internal/detector/gitlab/gitlab_token_test.go index 6bd6578..d496ab4 100644 --- a/internal/detector/gitlab/gitlab_token_test.go +++ b/internal/detector/gitlab/gitlab_token_test.go @@ -90,8 +90,14 @@ func TestDetector_Scan_RoutableTokenPrefixes(t *testing.T) { }{ {name: "deploy token", prefix: "gldt-", redacted: "gldt-****xyzW"}, {name: "runner token", prefix: "glrt-", redacted: "glrt-****xyzW"}, + {name: "runner authentication token", prefix: "glrtr-", redacted: "glrtr-****xyzW"}, {name: "ci build token", prefix: "glcbt-", redacted: "glcbt-****xyzW"}, {name: "pipeline trigger token", prefix: "glptt-", redacted: "glptt-****xyzW"}, + {name: "incoming mail token", prefix: "glimt-", redacted: "glimt-****xyzW"}, + {name: "agent token", prefix: "glagent-", redacted: "glagent-****xyzW"}, + {name: "workhorse token", prefix: "glwt-", redacted: "glwt-****xyzW"}, + {name: "service account token", prefix: "glsoat-", redacted: "glsoat-****xyzW"}, + {name: "feature flags client token", prefix: "glffct-", redacted: "glffct-****xyzW"}, {name: "oauth app secret", prefix: "gloas-", redacted: "gloas-****xyzW"}, {name: "feed token", prefix: "glft-", redacted: "glft-****xyzW"}, } @@ -107,46 +113,18 @@ func TestDetector_Scan_RoutableTokenPrefixes(t *testing.T) { } } -func TestDetector_Scan_CapturesSelfHostedHost(t *testing.T) { +func TestDetector_Scan_NeverTreatsRepositoryHostAsTrustedContext(t *testing.T) { token := "glpat-abcDEF1234567890xyzW" - - tests := []struct { - name string - input string - wantHost string - }{ - { - name: "self-hosted instance url", - input: "CI_SERVER_URL=https://gitlab.example.com\n" + token, - wantHost: "gitlab.example.com", - }, - { - name: "self-hosted with port", - input: "remote https://gitlab.corp.internal:8443/group/proj.git " + token, - wantHost: "gitlab.corp.internal:8443", - }, - { - name: "gitlab.com url", - input: "https://gitlab.com/acme/repo " + token, - wantHost: "gitlab.com", - }, - { - name: "no url co-located", - input: token, - wantHost: "", - }, + inputs := []string{ + "CI_SERVER_URL=https://gitlab.attacker.example\n" + token, + "CI_SERVER_URL=http://127.0.0.1:8080/gitlab\n" + token, + "CI_SERVER_URL=https://gitlab.corp.internal:8443\n" + token, + "CI_SERVER_URL=https://gitlab.com\n" + token, } - d := &Detector{} - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - findings := d.Scan(context.Background(), []byte(tt.input)) - require.Len(t, findings, 1) - if tt.wantHost == "" { - assert.Empty(t, findings[0].ExtraData["host"]) - return - } - assert.Equal(t, tt.wantHost, findings[0].ExtraData["host"]) - }) + for _, input := range inputs { + findings := d.Scan(context.Background(), []byte(input)) + require.Len(t, findings, 1) + assert.Empty(t, findings[0].ExtraData) } } diff --git a/internal/detector/registry.go b/internal/detector/registry.go index d874df8..b99650e 100644 --- a/internal/detector/registry.go +++ b/internal/detector/registry.go @@ -1,6 +1,7 @@ package detector import ( + "reflect" "sort" "sync" ) @@ -14,26 +15,29 @@ var ( // Each detector package calls this in its init() function. // Panics if a duplicate ID is registered. func Register(d Detector) { + if d == nil || isNilDetector(d) { + panic("cannot register a nil detector") + } + id := d.ID() + if id == "" { + panic("cannot register a detector with an empty ID") + } mu.Lock() defer mu.Unlock() - if _, exists := detectors[d.ID()]; exists { - panic("duplicate detector ID: " + d.ID()) + if _, exists := detectors[id]; exists { + panic("duplicate detector ID: " + id) } - detectors[d.ID()] = d + detectors[id] = d } -// RegisterIfAbsent registers d only if its ID is not already taken, atomically. -// It returns true when the detector was registered and false when an entry with -// the same ID already existed. Unlike Register it never panics, which makes it -// safe for runtime-supplied detectors such as user-defined custom rules. -func RegisterIfAbsent(d Detector) bool { - mu.Lock() - defer mu.Unlock() - if _, exists := detectors[d.ID()]; exists { +func isNilDetector(d Detector) bool { + value := reflect.ValueOf(d) + switch value.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice: + return value.IsNil() + default: return false } - detectors[d.ID()] = d - return true } // All returns all registered detectors sorted by ID. @@ -50,14 +54,6 @@ func All() []Detector { return result } -// Get returns the detector with the given ID. -func Get(id string) (Detector, bool) { - mu.RLock() - defer mu.RUnlock() - d, ok := detectors[id] - return d, ok -} - // Reset clears all registered detectors. For testing only. func Reset() { mu.Lock() diff --git a/internal/detector/registry_count_test.go b/internal/detector/registry_count_test.go index d11ffcd..570b26d 100644 --- a/internal/detector/registry_count_test.go +++ b/internal/detector/registry_count_test.go @@ -7,12 +7,11 @@ package detector_test // (each subpackage imports the detector package under test). // // Counts measured from the codebase: -// - 64 detectors registered at compile time via init() (detector.Register). -// - 59 packages register statically; azure, github, slack, stripe and -// discord each register two detectors (59 + 5 = 64). -// - 60 detector subpackages exist in total; the 60th, "custom", registers its -// rules at runtime (detector.RegisterIfAbsent) and is therefore not part of -// the compile-time count. +// - 65 detectors registered at compile time via init() (detector.Register). +// - 59 packages register statically; azure, github, slack, stripe, discord +// and generic each register two detectors (59 + 6 = 65). +// - 60 detector subpackages exist in total; the 60th, "custom", is compiled +// into the per-scan detector slice and never mutates this global registry. // // If you add or remove a detector, update internal/meta.Detectors (the single // source of truth for the published count) and keep the blank-import block in @@ -24,9 +23,11 @@ import ( "fmt" "os" "path/filepath" + "reflect" "testing" "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/internal/detector/testutil" "github.com/HodeTech/leakwatch/internal/meta" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -55,7 +56,7 @@ import ( _ "github.com/HodeTech/leakwatch/internal/detector/figma" // register figma detector _ "github.com/HodeTech/leakwatch/internal/detector/ftp" // register ftp credentials detector _ "github.com/HodeTech/leakwatch/internal/detector/gcp" // register gcp service-account detector - _ "github.com/HodeTech/leakwatch/internal/detector/generic" // register generic api-key detector + _ "github.com/HodeTech/leakwatch/internal/detector/generic" // register generic API-key and structured-config detectors _ "github.com/HodeTech/leakwatch/internal/detector/github" // register github detectors (pat + oauth) _ "github.com/HodeTech/leakwatch/internal/detector/gitlab" // register gitlab detector _ "github.com/HodeTech/leakwatch/internal/detector/grafana" // register grafana detector @@ -119,16 +120,76 @@ func TestAll_RegisteredDetectorCount_MatchesGolden(t *testing.T) { } } +// TestAll_RegisteredDetectors_HaveMatcherParity is the registry-wide recall +// contract. Every compile-time detector must have an explicit, synthetic +// positive fixture that reaches the detector through the same matcher gate the +// engine uses in production. Keeping the fixture catalog keyed by detector ID +// makes a newly registered detector fail closed until its matcher contract is +// reviewed and documented here. +func TestAll_RegisteredDetectors_HaveMatcherParity(t *testing.T) { + fixtures := testutil.RegisteredDetectorFixtures() + require.Len(t, fixtures, len(registeredAtInit), + "every registered detector needs exactly one matcher-contract fixture") + + registered := make(map[string]detector.Detector, len(registeredAtInit)) + for _, det := range registeredAtInit { + registered[det.ID()] = det + } + for id := range fixtures { + assert.Contains(t, registered, id, "stale fixture for an unregistered detector") + } + + for id, det := range registered { + det := det + fixture, ok := fixtures[id] + require.True(t, ok, "registered detector %q has no matcher-contract fixture", id) + t.Run(id, func(t *testing.T) { + direct := det.Scan(t.Context(), fixture.Input) + require.Len(t, direct, 1, "canonical fixture must exercise exactly one unambiguous finding") + assert.Equal(t, id, direct[0].DetectorID, "detector returned a finding owned by a different capability") + assert.Equal(t, fixture.ExpectedRaw, direct[0].Raw, "detector widened or changed the canonical raw credential") + assert.Equal(t, fixture.ExpectedRawV2, direct[0].RawV2, "detector companion/raw-v2 contract drifted") + assert.Equal(t, fixture.ExpectedExtraData, direct[0].ExtraData, "detector context metadata contract drifted") + assert.NotEqual(t, string(direct[0].Raw), direct[0].Redacted, "redacted output must not equal the raw credential") + assert.NotContains(t, direct[0].Redacted, string(direct[0].Raw), "redacted output leaked the raw credential") + for key, value := range direct[0].ExtraData { + if !fixture.NonSecretRawExtraKeys[key] { + assert.NotContains(t, value, string(direct[0].Raw), "ExtraData[%q] leaked the raw credential", key) + } + if len(direct[0].RawV2) > 0 { + assert.NotContains(t, value, string(direct[0].RawV2), "ExtraData[%q] leaked RawV2", key) + } + } + if fixture.RequireExactSpan { + require.Greater(t, direct[0].ByteEnd, direct[0].ByteStart, "explicit source span must be non-empty") + assert.Equal(t, direct[0].Raw, fixture.Input[direct[0].ByteStart:direct[0].ByteEnd], + "explicit source span must select the exact raw finding bytes") + } else { + assert.Zero(t, direct[0].ByteStart, "span contract is explicitly absent for this detector") + assert.Zero(t, direct[0].ByteEnd, "span contract is explicitly absent for this detector") + } + + viaMatcher := testutil.ScanViaMatcher(det, fixture.Input) + require.NotEmpty(t, viaMatcher, + "matcher gated out a fixture accepted by Scan; review Keywords and case/boundary variants") + assert.True(t, reflect.DeepEqual(direct, viaMatcher), + "direct Scan and matcher-routed Scan returned different findings\ndirect: %#v\nmatcher: %#v", + direct, viaMatcher) + }) + } +} + // playgroundSkippedIDs are registered detectors intentionally absent from the // generated site/js/detectors.js bundle. tools/site-build skips the "generic", // "custom", and "testutil" detector packages because the in-browser regex // scanner cannot reproduce their detection faithfully (see // tools/site-build/detectors.go detectorSkipDirs). Of those, only the generic -// detector is registered at compile time, so it is the sole expected omission; -// "custom" registers at runtime (not in detector.All()) and "testutil" is a test -// helper, not a detector. +// package is registered at compile time with two detectors, so both are +// expected omissions; "custom" registers at runtime (not in detector.All()) +// and "testutil" is a test helper, not a detector. var playgroundSkippedIDs = map[string]bool{ - "generic-api-key": true, + "generic-api-key": true, + "structured-config-secret": true, } // TestDetectorsJS_CoversEveryRegisteredDetector guards the generated playground diff --git a/internal/detector/registry_test.go b/internal/detector/registry_test.go index 522ffb6..934110e 100644 --- a/internal/detector/registry_test.go +++ b/internal/detector/registry_test.go @@ -2,7 +2,6 @@ package detector import ( "context" - "sync" "testing" "github.com/HodeTech/leakwatch/pkg/finding" @@ -34,23 +33,6 @@ func TestRegister_AndAll_ReturnsAllDetectors(t *testing.T) { assert.Len(t, all, 2) } -func TestGet_ExistingDetector_ReturnsDetector(t *testing.T) { - Reset() - - Register(&mockDetector{id: "test-1"}) - - d, ok := Get("test-1") - require.True(t, ok) - assert.Equal(t, "test-1", d.ID()) -} - -func TestGet_NonExistingDetector_ReturnsFalse(t *testing.T) { - Reset() - - _, ok := Get("not-found") - assert.False(t, ok) -} - func TestRegister_DuplicateID_Panics(t *testing.T) { Reset() @@ -61,6 +43,15 @@ func TestRegister_DuplicateID_Panics(t *testing.T) { }) } +func TestRegister_NilOrEmptyID_PanicsWithoutMutatingRegistry(t *testing.T) { + Reset() + var typedNil *mockDetector + assert.Panics(t, func() { Register(nil) }) + assert.Panics(t, func() { Register(typedNil) }) + assert.Panics(t, func() { Register(&mockDetector{}) }) + assert.Empty(t, All()) +} + func TestAll_EmptyRegistry_ReturnsEmpty(t *testing.T) { Reset() @@ -83,92 +74,3 @@ func TestAll_MultipleDetectors_ReturnsSortedByID(t *testing.T) { assert.Equal(t, "middle-detector", all[1].ID()) assert.Equal(t, "zebra-detector", all[2].ID()) } - -func TestRegisterIfAbsent_NewID_RegistersAndReturnsTrue(t *testing.T) { - Reset() - - ok := RegisterIfAbsent(&mockDetector{id: "new-detector"}) - assert.True(t, ok) - - d, found := Get("new-detector") - require.True(t, found) - assert.Equal(t, "new-detector", d.ID()) -} - -func TestRegisterIfAbsent_DuplicateID_ReturnsFalseWithoutPanicOrOverwrite(t *testing.T) { - Reset() - - original := &mockDetector{id: "dup-detector"} - Register(original) - - assert.NotPanics(t, func() { - ok := RegisterIfAbsent(&mockDetector{id: "dup-detector"}) - assert.False(t, ok) - }) - - // The original registration must be untouched by the rejected attempt. - d, found := Get("dup-detector") - require.True(t, found) - assert.Same(t, original, d) - - all := All() - assert.Len(t, all, 1) -} - -// TestRegisterIfAbsent_ConcurrentDuplicateIDs_ExactlyOneWinsAtomically drives -// many goroutines racing to register the same ID via RegisterIfAbsent, and -// asserts the registry ends up with exactly one entry with no data race -// (run with -race). This locks down the "atomically" guarantee documented on -// RegisterIfAbsent for runtime-supplied detectors such as custom rules, -// which may be registered concurrently with other scan setup. -func TestRegisterIfAbsent_ConcurrentDuplicateIDs_ExactlyOneWinsAtomically(t *testing.T) { - Reset() - - const goroutines = 50 - var wg sync.WaitGroup - var successCount int32 - var mu sync.Mutex - - wg.Add(goroutines) - for i := 0; i < goroutines; i++ { - go func(n int) { - defer wg.Done() - if RegisterIfAbsent(&mockDetector{id: "concurrent-dup"}) { - mu.Lock() - successCount++ - mu.Unlock() - } - }(i) - } - wg.Wait() - - assert.Equal(t, int32(1), successCount, "exactly one registration must win") - - all := All() - assert.Len(t, all, 1) - assert.Equal(t, "concurrent-dup", all[0].ID()) -} - -// TestRegisterIfAbsent_ConcurrentDistinctIDs_AllSucceed exercises concurrent -// registration of distinct IDs to ensure no entries are lost or corrupted -// under concurrent access (run with -race). -func TestRegisterIfAbsent_ConcurrentDistinctIDs_AllSucceed(t *testing.T) { - Reset() - - const goroutines = 50 - var wg sync.WaitGroup - - wg.Add(goroutines) - for i := 0; i < goroutines; i++ { - go func(n int) { - defer wg.Done() - id := "detector-" + string(rune('a'+n%26)) + string(rune('0'+n/26)) - ok := RegisterIfAbsent(&mockDetector{id: id}) - assert.True(t, ok) - }(i) - } - wg.Wait() - - all := All() - assert.Len(t, all, goroutines) -} diff --git a/internal/detector/shopify/shopify_token.go b/internal/detector/shopify/shopify_token.go index 121fd06..2ff82f8 100644 --- a/internal/detector/shopify/shopify_token.go +++ b/internal/detector/shopify/shopify_token.go @@ -9,7 +9,7 @@ import ( "github.com/HodeTech/leakwatch/pkg/finding" ) -var shopifyTokenPattern = regexp.MustCompile(`shpat_[a-f0-9]{32}`) +var shopifyTokenPattern = regexp.MustCompile(`\bshpat_[a-f0-9]{32}\b`) // Detector detects Shopify Access Tokens. type Detector struct{} @@ -22,20 +22,27 @@ func (d *Detector) Keywords() []string { return []string{"shpat_"} } func (d *Detector) Severity() finding.Severity { return finding.SeverityCritical } +// AuthoritativeOnOverlap lets the engine prefer this provider-specific token +// over a broad structured-field finding for the same source bytes. +func (d *Detector) AuthoritativeOnOverlap() bool { return true } + // Scan searches the data for Shopify Access Token patterns. func (d *Detector) Scan(_ context.Context, data []byte) []detector.RawFinding { - matches := shopifyTokenPattern.FindAll(data, -1) + matches := shopifyTokenPattern.FindAllIndex(data, -1) if len(matches) == 0 { return nil } findings := make([]detector.RawFinding, 0, len(matches)) - for _, match := range matches { + for _, loc := range matches { + match := data[loc[0]:loc[1]] s := string(match) findings = append(findings, detector.RawFinding{ DetectorID: d.ID(), - Raw: match, + Raw: append([]byte(nil), match...), Redacted: "shpat_****" + s[len(s)-4:], + ByteStart: loc[0], + ByteEnd: loc[1], }) } return findings diff --git a/internal/detector/shopify/shopify_token_test.go b/internal/detector/shopify/shopify_token_test.go index e7cba87..080810e 100644 --- a/internal/detector/shopify/shopify_token_test.go +++ b/internal/detector/shopify/shopify_token_test.go @@ -5,6 +5,7 @@ import ( "strings" "testing" + "github.com/HodeTech/leakwatch/internal/detector/testutil" "github.com/HodeTech/leakwatch/pkg/finding" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -16,6 +17,27 @@ func TestDetector_Metadata_ReturnsExpectedValues(t *testing.T) { assert.Equal(t, "Shopify Access Token", d.Description()) assert.Equal(t, finding.SeverityCritical, d.Severity()) assert.NotEmpty(t, d.Keywords()) + assert.True(t, d.AuthoritativeOnOverlap()) +} + +func TestDetector_ScanViaMatcher_MatchesExactToken(t *testing.T) { + suffix32 := strings.Repeat("ab12cd34", 4) + findings := testutil.ScanViaMatcher(&Detector{}, []byte("SHOPIFY_ACCESS_TOKEN=shpat_"+suffix32)) + + require.Len(t, findings, 1) + assert.Equal(t, []byte("shpat_"+suffix32), findings[0].Raw) + assert.Equal(t, strings.Index("SHOPIFY_ACCESS_TOKEN=shpat_"+suffix32, "shpat_"), findings[0].ByteStart) + assert.Equal(t, string(findings[0].Raw), ("SHOPIFY_ACCESS_TOKEN=shpat_" + suffix32)[findings[0].ByteStart:findings[0].ByteEnd]) +} + +func TestDetector_Scan_ExactSpanSelectsAcceptedOccurrence(t *testing.T) { + token := "shpat_" + strings.Repeat("ab12cd34", 4) + input := "prefix_" + token + " # rejected duplicate\nSHOPIFY_ACCESS_TOKEN=" + token + + findings := (&Detector{}).Scan(context.Background(), []byte(input)) + require.Len(t, findings, 1) + assert.Equal(t, strings.LastIndex(input, token), findings[0].ByteStart) + assert.Equal(t, token, input[findings[0].ByteStart:findings[0].ByteEnd]) } func TestDetector_Scan_MatchAndReject(t *testing.T) { @@ -54,6 +76,16 @@ func TestDetector_Scan_MatchAndReject(t *testing.T) { input: "shpas_" + suffix32, expected: 0, }, + { + name: "no match - longer candidate", + input: "shpat_" + suffix32 + "a", + expected: 0, + }, + { + name: "no match - embedded in identifier", + input: "prefix_shpat_" + suffix32, + expected: 0, + }, { name: "no match - plain text", input: "this is just normal text", diff --git a/internal/detector/supabase/supabase_key.go b/internal/detector/supabase/supabase_key.go index 6f786c4..68e42ca 100644 --- a/internal/detector/supabase/supabase_key.go +++ b/internal/detector/supabase/supabase_key.go @@ -1,4 +1,4 @@ -// Package supabase provides a Supabase Service Role Key secret detector. +// Package supabase provides a Supabase personal access token detector. package supabase import ( @@ -9,20 +9,22 @@ import ( "github.com/HodeTech/leakwatch/pkg/finding" ) -var supabaseKeyPattern = regexp.MustCompile(`sbp_[a-f0-9]{40}`) +var supabaseKeyPattern = regexp.MustCompile(`\bsbp_[a-f0-9]{40}\b`) -// Detector detects Supabase Service Role Keys. +// Detector detects legacy Supabase Management API personal access tokens. The +// stable detector ID predates this naming correction; sbp_ is not a project +// service-role key. type Detector struct{} func (d *Detector) ID() string { return "supabase-service-key" } -func (d *Detector) Description() string { return "Supabase Service Role Key" } +func (d *Detector) Description() string { return "Supabase Personal Access Token" } func (d *Detector) Keywords() []string { return []string{"sbp_"} } func (d *Detector) Severity() finding.Severity { return finding.SeverityCritical } -// Scan searches the data for Supabase Service Role Key patterns. +// Scan searches the data for Supabase personal access token patterns. func (d *Detector) Scan(_ context.Context, data []byte) []detector.RawFinding { matches := supabaseKeyPattern.FindAll(data, -1) if len(matches) == 0 { diff --git a/internal/detector/supabase/supabase_key_test.go b/internal/detector/supabase/supabase_key_test.go index 64931e2..d2ee63c 100644 --- a/internal/detector/supabase/supabase_key_test.go +++ b/internal/detector/supabase/supabase_key_test.go @@ -13,7 +13,7 @@ import ( func TestDetector_Metadata_ReturnsExpectedValues(t *testing.T) { d := &Detector{} assert.Equal(t, "supabase-service-key", d.ID()) - assert.Equal(t, "Supabase Service Role Key", d.Description()) + assert.Equal(t, "Supabase Personal Access Token", d.Description()) assert.Equal(t, finding.SeverityCritical, d.Severity()) assert.NotEmpty(t, d.Keywords()) } @@ -36,7 +36,7 @@ func TestDetector_Scan_MatchAndReject(t *testing.T) { }, { name: "key embedded in env var", - input: `SUPABASE_SERVICE_ROLE_KEY=sbp_` + suffix40, + input: `SUPABASE_ACCESS_TOKEN=sbp_` + suffix40, expected: 1, }, { @@ -54,6 +54,16 @@ func TestDetector_Scan_MatchAndReject(t *testing.T) { input: "sbq_" + suffix40, expected: 0, }, + { + name: "no match - suffix longer than forty hex characters", + input: "sbp_" + suffix40 + "a", + expected: 0, + }, + { + name: "no match - token embedded in identifier", + input: "prefix_sbp_" + suffix40, + expected: 0, + }, { name: "no match - plain text", input: "this is just normal text", diff --git a/internal/detector/testutil/fixtures.go b/internal/detector/testutil/fixtures.go new file mode 100644 index 0000000..4754b21 --- /dev/null +++ b/internal/detector/testutil/fixtures.go @@ -0,0 +1,183 @@ +package testutil + +import "strings" + +// DetectorContractFixture pins the observable contract of one canonical +// detector example. Expectations are deliberately explicit: a detector cannot +// widen Raw, drop an exact span, or move credential bytes into ExtraData while +// retaining a green registry-parity test. +type DetectorContractFixture struct { + Input []byte + ExpectedRaw []byte + ExpectedRawV2 []byte + ExpectedExtraData map[string]string + // NonSecretRawExtraKeys explicitly documents rare fields where Raw is an + // identifier rather than credential material (for example a GCP private key + // ID) and may therefore also appear as non-secret verifier context. + NonSecretRawExtraKeys map[string]bool + RequireExactSpan bool +} + +// RegisteredDetectorFixtures returns one deliberately non-functional positive +// fixture for every compile-time detector. The catalog is shared by the +// registry-wide matcher test and detector-to-verifier contract test so neither +// layer can prove compatibility with hand-built RawFinding values that a real +// detector never emits. +func RegisteredDetectorFixtures() map[string]DetectorContractFixture { + repeat := strings.Repeat + assigned := func(prefix, raw string, span bool) DetectorContractFixture { + return DetectorContractFixture{Input: []byte(prefix + raw), ExpectedRaw: []byte(raw), RequireExactSpan: span} + } + whole := func(input string) DetectorContractFixture { + return DetectorContractFixture{Input: []byte(input), ExpectedRaw: []byte(input)} + } + paired := func(input, raw, rawV2 string, extra map[string]string, span bool) DetectorContractFixture { + fixture := DetectorContractFixture{ + Input: []byte(input), ExpectedRaw: []byte(raw), + ExpectedExtraData: extra, RequireExactSpan: span, + } + if rawV2 != "" { + fixture.ExpectedRawV2 = []byte(rawV2) + } + return fixture + } + allowRawExtra := func(fixture DetectorContractFixture, keys ...string) DetectorContractFixture { + fixture.NonSecretRawExtraKeys = make(map[string]bool, len(keys)) + for _, key := range keys { + fixture.NonSecretRawExtraKeys[key] = true + } + return fixture + } + return map[string]DetectorContractFixture{ + "airtable-pat": assigned("AIRTABLE_TOKEN=", "patAb12Cd34Ef56Gh."+repeat("ab12cd34", 8), false), + "anthropic-api-key": assigned("ANTHROPIC_API_KEY=", "sk-ant-"+repeat("Ab12Cd34_", 10), false), + "auth0-management-token": paired( + "AUTH0_MANAGEMENT_TOKEN=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2ZpeHR1cmUuZXUuYXV0aDAuY29tLyJ9.c2lnbmF0dXJlLWZpeHR1cmU", + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2ZpeHR1cmUuZXUuYXV0aDAuY29tLyJ9.c2lnbmF0dXJlLWZpeHR1cmU", + "AUTH0_MANAGEMENT_TOKEN=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2ZpeHR1cmUuZXUuYXV0aDAuY29tLyJ9.c2lnbmF0dXJlLWZpeHR1cmU", nil, true, + ), + "aws-access-key-id": paired( + "aws_access_key_id=AKIAQ7M2PL9RT4VW8XYZ\naws_secret_access_key="+repeat("Ab1/", 10), + "AKIAQ7M2PL9RT4VW8XYZ", repeat("Ab1/", 10), nil, false, + ), + "azure-entra-secret": paired( + "AZURE_CLIENT_SECRET="+repeat("Ab12Cd34", 5), repeat("Ab12Cd34", 5), + "AZURE_CLIENT_SECRET="+repeat("Ab12Cd34", 5), nil, false, + ), + "azure-storage-key": paired( + "DefaultEndpointsProtocol=https;AccountName=syntheticacct;AccountKey="+repeat("Ab12Cd34", 11)+";", + "DefaultEndpointsProtocol=https;AccountName=syntheticacct;AccountKey="+repeat("Ab12Cd34", 11)+";", "", + map[string]string{"account_name": "syntheticacct"}, false, + ), + "bitbucket-app-password": paired( + "BITBUCKET_USERNAME=fixture-user\nBITBUCKET_APP_PASSWORD="+repeat("Ab12", 5), + repeat("Ab12", 5), "BITBUCKET_APP_PASSWORD="+repeat("Ab12", 5), + map[string]string{"username": "fixture-user"}, false, + ), + "circleci-token": assigned("CIRCLECI_TOKEN=", "CCIPAT_"+repeat("Ab12Cd34_", 6), false), + "cloudflare-api-token": paired( + "CLOUDFLARE_API_TOKEN="+repeat("Ab12Cd34", 5), repeat("Ab12Cd34", 5), + "CLOUDFLARE_API_TOKEN="+repeat("Ab12Cd34", 5), nil, false, + ), + "coinbase-api-key": paired( + "COINBASE_API_KEY="+repeat("Ab12Cd34", 4), repeat("Ab12Cd34", 4), + "COINBASE_API_KEY="+repeat("Ab12Cd34", 4), nil, false, + ), + "database-connection-string": whole("postgres://fixture_user:synthetic_password@db.example.invalid:5432/app"), + "databricks-token": paired( + "DATABRICKS_HOST=https://fixture.cloud.databricks.com\nDATABRICKS_TOKEN=dapi"+repeat("ab12cd34", 4), + "dapi"+repeat("ab12cd34", 4), "", map[string]string{"host": "https://fixture.cloud.databricks.com"}, false, + ), + "datadog-api-key": paired( + "DATADOG_API_KEY="+repeat("ab12cd34", 4), repeat("ab12cd34", 4), + "DATADOG_API_KEY="+repeat("ab12cd34", 4), nil, false, + ), + "deepseek-api-key": assigned("DEEPSEEK_API_KEY=", "sk-"+repeat("ab12cd34", 4), false), + "digitalocean-token": assigned("DIGITALOCEAN_TOKEN=", "dop_v1_"+repeat("ab12cd34", 8), false), + "discord-bot-token": assigned("DISCORD_TOKEN=", "M"+repeat("Ab1c", 5)+"Xyz.Ab1Cd2."+repeat("Abc1", 6)+"XyZ", false), + "discord-webhook-url": paired( + "https://discord.com/api/webhooks/123456789012345678/"+repeat("Ab12Cd34_", 4), + "discord.com/api/webhooks/123456789012345678/"+repeat("Ab12Cd34_", 4), "", nil, false, + ), + "dockerhub-pat": assigned("DOCKER_TOKEN=", "dckr_pat_"+repeat("Ab12Cd34_", 4), false), + "doppler-token": assigned("DOPPLER_TOKEN=", "dp.st."+repeat("Ab12Cd34_", 5), false), + "figma-pat": assigned("FIGMA_TOKEN=", "figd_"+repeat("Ab12Cd34_", 5), false), + "ftp-credentials": whole("ftp://fixture-user:synthetic-password@ftp.example.invalid:21/uploads"), + "gcp-service-account": allowRawExtra(paired( + `{"type":"service_account","project_id":"fixture-project","private_key_id":"abcdef1234567890abcdef1234567890abcdef12","client_email":"fixture@fixture-project.iam.gserviceaccount.com"}`, + "abcdef1234567890abcdef1234567890abcdef12", + `{"type":"service_account","project_id":"fixture-project","private_key_id":"abcdef1234567890abcdef1234567890abcdef12","client_email":"fixture@fixture-project.iam.gserviceaccount.com"}`, + map[string]string{"client_email": "fixture@fixture-project.iam.gserviceaccount.com", "private_key_id": "abcdef1234567890abcdef1234567890abcdef12"}, false, + ), "private_key_id"), + "generic-api-key": paired( + `api_key = "Q7mN2pL9rT4vW8xYzC6bH3kF"`, "Q7mN2pL9rT4vW8xYzC6bH3kF", "", + map[string]string{"key_name": "api_key"}, true, + ), + "github-oauth-token": paired( + "GITHUB_TOKEN=gho_"+repeat("Ab12Cd34_", 4), "gho_"+repeat("Ab12Cd34_", 4), "", + map[string]string{"token_subtype": "gho"}, false, + ), + "github-token": assigned("GITHUB_TOKEN=", "ghp_"+repeat("Ab12Cd34_", 4), false), + "gitlab-pat": assigned("GITLAB_TOKEN=", "glpat-"+repeat("Ab12Cd34_", 3), false), + "grafana-api-key": assigned("GRAFANA_TOKEN=", "glsa_"+repeat("Ab12Cd34", 4)+"_abcdef12", false), + "hashicorp-vault-token": assigned("VAULT_TOKEN=", "hvs."+repeat("Ab12Cd34_", 3), false), + "heroku-api-key": paired( + "HEROKU_API_KEY=12345678-1234-abcd-9876-1234567890ab", "12345678-1234-abcd-9876-1234567890ab", + "HEROKU_API_KEY=12345678-1234-abcd-9876-1234567890ab", nil, false, + ), + "huggingface-token": assigned("HF_TOKEN=", "hf_"+repeat("Ab12Cd34", 5), false), + "infura-api-key": paired( + "INFURA_API_KEY="+repeat("ab12cd34", 4), repeat("ab12cd34", 4), + "INFURA_API_KEY="+repeat("ab12cd34", 4), nil, false, + ), + "jwt": assigned("Authorization: Bearer ", "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0."+repeat("Ab12Cd34", 4), false), + "launchdarkly-sdk-key": assigned("LAUNCHDARKLY_SDK_KEY=", "sdk-abcdef01-2345-6789-abcd-0123456789ab", false), + "ldap-credentials": whole("ldaps://fixture-user:synthetic-password@ldap.example.invalid:636/dc=example,dc=invalid"), + "linear-api-key": assigned("LINEAR_API_KEY=", "lin_api_"+repeat("Ab12Cd34", 5), false), + "mailgun-api-key": assigned("MAILGUN_API_KEY=", "key-"+repeat("ab12cd34", 4), false), + "newrelic-api-key": assigned("NEW_RELIC_API_KEY=", "NRAK-"+repeat("AB12CD34X", 3), false), + "notion-token": assigned("NOTION_TOKEN=", "ntn_"+repeat("Ab12Cd34", 6), false), + "npm-token": assigned("NPM_TOKEN=", "npm_"+repeat("Ab12Cd34X", 4), false), + "okta-api-token": paired( + "OKTA_ORG=https://fixture.okta.com\nAuthorization: SSWS 00"+repeat("Ab12Cd34", 5), + "00"+repeat("Ab12Cd34", 5), "", map[string]string{"domain": "fixture.okta.com"}, false, + ), + "openai-api-key": assigned("OPENAI_API_KEY=", "sk-proj-"+repeat("Ab12Cd34_", 6), false), + "pagerduty-api-key": assigned("PAGERDUTY_API_KEY=", "u+"+repeat("Ab12Cd34_", 3), false), + "postmark-server-token": assigned("POSTMARK_SERVER_TOKEN=", "abcdef01-2345-6789-abcd-0123456789ab", false), + "private-key": paired( + "-----BEGIN PRIVATE KEY-----\nSYNTHETIC-NOT-KEY-MATERIAL\n-----END PRIVATE KEY-----", + "-----BEGIN PRIVATE KEY-----", "", map[string]string{"block_bytes": "80"}, false, + ), + "pypi-api-token": assigned("PYPI_TOKEN=", "pypi-AgEIcHlwaS5vcmc"+repeat("Ab12Cd34_", 7), false), + "rabbitmq-connection-string": whole("amqps://fixture-user:synthetic-password@rabbitmq.example.invalid:5671/vhost"), + "redis-connection-string": whole("rediss://fixture-user:synthetic-password@cache.example.invalid:6380/0"), + "rubygems-api-key": assigned("RUBYGEMS_API_KEY=", "rubygems_"+repeat("ab12cd34", 6), false), + "sendgrid-api-key": assigned("SENDGRID_API_KEY=", "SG."+repeat("Ab", 11)+"."+repeat("Ab", 21)+"A", false), + "sentry-token": assigned("SENTRY_AUTH_TOKEN=", "sntrys_"+repeat("Ab12Cd34_", 5), false), + "shopify-access-token": assigned("SHOPIFY_ACCESS_TOKEN=", "shpat_"+repeat("ab12cd34", 4), true), + "slack-token": assigned("SLACK_TOKEN=", "xoxb-123456789012-123456789012-"+repeat("Ab12Cd34", 3), false), + "slack-webhook": whole("https://hooks.slack.com/services/TABCDEF12/BABCDEF12/" + repeat("Ab12Cd34", 3)), + "snowflake-credentials": paired( + "account.snowflakecomputing.com/?warehouse=fixture&password=synthetic-password", + "synthetic-password", "snowflakecomputing.com/?warehouse=fixture&password=synthetic-password", nil, false, + ), + "snyk-api-key": assigned("SNYK_TOKEN=", "abcdef01-2345-6789-abcd-0123456789ab", false), + "sonarcloud-token": assigned("SONAR_TOKEN=", "sqp_"+repeat("ab12cd34", 5), false), + "stripe-api-key-live": assigned("STRIPE_SECRET_KEY=", "sk_live_"+repeat("Ab12Cd34", 3), false), + "stripe-api-key-test": assigned("STRIPE_TEST_KEY=", "sk_test_"+repeat("Ab12Cd34", 3), false), + "structured-config-secret": paired( + `{"Database":{"Password":"Q7mN2pL9rT4vW8xYzC6bH3kF"}}`, "Q7mN2pL9rT4vW8xYzC6bH3kF", "", + map[string]string{"key_name": "Password", "key_path": "Database.Password", "config_format": "json"}, true, + ), + "supabase-service-key": assigned("SUPABASE_ACCESS_TOKEN=", "sbp_"+repeat("ab12cd34", 5), false), + "teams-webhook": whole("https://fixture.webhook.office.com/webhookb2/abcdef01-2345-6789-abcd-ef0123456789/IncomingWebhook/abcdef0123456789/abcdef01-2345-6789-abcd-ef0123456789"), + "telegram-bot-token": assigned("TELEGRAM_BOT_TOKEN=", "123456789:"+repeat("Ab12Cd3", 5), false), + "terraform-cloud-token": assigned("TERRAFORM_TOKEN=", repeat("Ab12Cd3", 2)+".atlasv1."+repeat("Ab12Cd34", 9), false), + "twilio-api-key": paired( + "TWILIO_API_KEY_SID=SK"+repeat("ab12cd34", 4)+"\nTWILIO_API_KEY_SECRET=synthetic-opaque-value-Ab12Cd34", + "synthetic-opaque-value-Ab12Cd34", "", map[string]string{"api_key_sid": "SK" + repeat("ab12cd34", 4)}, true, + ), + "vercel-token": assigned("VERCEL_TOKEN=", "vercel_"+repeat("Ab12Cd34_", 3), false), + } +} diff --git a/internal/detector/twilio/twilio_key.go b/internal/detector/twilio/twilio_key.go index 765665f..04d9f30 100644 --- a/internal/detector/twilio/twilio_key.go +++ b/internal/detector/twilio/twilio_key.go @@ -1,114 +1,282 @@ -// Package twilio provides a Twilio API Key secret detector. +// Package twilio provides a detector for paired Twilio API Key credentials. package twilio import ( "bytes" "context" "regexp" + "strings" + "unicode/utf8" "github.com/HodeTech/leakwatch/internal/detector" "github.com/HodeTech/leakwatch/pkg/finding" ) var ( - twilioKeyPattern = regexp.MustCompile(`SK[a-f0-9]{32}`) - twilioSIDPattern = regexp.MustCompile(`AC[a-f0-9]{32}`) + // An SK value is an API Key SID: a public identifier, not a secret. It is + // used only as companion context when it is assigned to an API Key SID role. + twilioKeySIDAssignmentPattern = regexp.MustCompile(`(?i)(?:^|[^0-9A-Za-z_])(?:(?:[a-z0-9]+[._-]+)*twilio[._-]*|x[._-]*)?api[._-]*(?:key[._-]*)?sid["']?\s*[:=]\s*["']?(SK[0-9a-f]{32})(?:["']|[ \t\r\n,;}#]|$)`) + twilioAccountSIDAssignmentPattern = regexp.MustCompile(`(?i)(?:^|[^0-9A-Za-z_])(?:(?:[a-z0-9]+[._-]+)*twilio[._-]*)?account[._-]*sid["']?\s*[:=]\s*["']?(AC[0-9a-f]{32})(?:["']|[ \t\r\n,;}#]|$)`) + + // Twilio documents API Key Secrets as opaque values and does not promise a + // fixed length or alphabet. These alternatives preserve an exact value span + // for double-quoted, single-quoted, and bounded unquoted assignments. + twilioAPISecretAssignmentPattern = regexp.MustCompile(`(?i)(?:^|[^0-9A-Za-z_])(?:(?:[a-z0-9]+[._-]+)*twilio[._-]*|x[._-]*)?api[._-]*(?:key[._-]*)?secret["']?\s*[:=]\s*(?:"([^"\r\n]{1,512})"|'([^'\r\n]{1,512})'|([^"' \t\r\n,;}#]{1,512})(?:[ \t\r\n,;}#]|$))`) + + logicalBlockSeparatorPattern = regexp.MustCompile(`(?:\r?\n[ \t]*\r?\n|\r?\n[ \t]*\[[^\r\n]+\][ \t]*(?:\r?\n|$))`) ) -// sidProximityWindow bounds how far (in bytes) from a key match an Account SID -// may sit to be considered part of the same credential. It keeps the SID -// correlation local so that unrelated key/SID pairs in the same chunk are not -// cross-attributed. -const sidProximityWindow = 512 +// companionProximityWindow bounds how far a non-secret SID may sit from the +// secret assignment to be treated as the same credential pair. +const companionProximityWindow = 512 -// Detector detects Twilio API Keys. +// Detector detects Twilio API Key Secrets only when paired with a nearby API +// Key SID. A bare SK SID is intentionally not a finding because it is not +// confidential and cannot authenticate to Twilio. type Detector struct{} -// ID returns the unique identifier of the Twilio API Key detector. +// ID returns the stable detector identifier. func (d *Detector) ID() string { return "twilio-api-key" } -// Description returns a human-readable description of the Twilio API Key detector. -func (d *Detector) Description() string { return "Twilio API Key" } +// Description returns a human-readable description. +func (d *Detector) Description() string { return "Twilio API Key Secret" } -// Keywords returns the Aho-Corasick pre-filter keywords for Twilio API Key -// detection. The SK API-Key SID pattern is self-contained (a fixed "SK" prefix -// plus 32 hex chars) and carries no guaranteed nearby literal, so returning no -// keywords forces the regex to run on every chunk. Requiring a "twilio" keyword -// would silently gate out standalone keys (e.g. in a secrets-manager dump or -// under a differently named variable). -func (d *Detector) Keywords() []string { return []string{} } +// Keywords returns the invariant role component shared by every supported +// secret assignment. The matcher lowercases chunks before keyword selection, +// so this skips unrelated files without losing casing or separator variants. +func (d *Detector) Keywords() []string { return []string{"secret"} } -// Severity returns the default severity level for Twilio API Key findings. +// Severity returns the default severity level for a leaked API Key Secret. func (d *Detector) Severity() finding.Severity { return finding.SeverityCritical } -// Scan searches the data for Twilio API Key patterns. -// Account SID (AC prefix) is captured as ExtraData rather than a separate -// finding. The SID is correlated per key match by proximity, so unrelated -// key/SID pairs in the same chunk are not cross-attributed. +// AuthoritativeOnOverlap lets the engine prefer this provider-specific paired +// finding over a structured-config fallback for the same secret bytes. +func (d *Detector) AuthoritativeOnOverlap() bool { return true } + +// PlaygroundPatternContract prevents the site generator from publishing the +// SID and secret-assignment regexes as independent OR triggers. +func (d *Detector) PlaygroundPatternContract() detector.PlaygroundPatternContract { + return detector.PlaygroundPatternContract{ + Primary: []*regexp.Regexp{twilioAPISecretAssignmentPattern}, + RequiredNearby: []*regexp.Regexp{twilioKeySIDAssignmentPattern}, + ProximityBytes: companionProximityWindow, + SameLogicalBlock: true, + RejectPlaceholders: true, + OneToOne: true, + } +} + +var _ detector.PlaygroundCorrelated = (*Detector)(nil) + +// Scan reports the secret value as Raw and carries only non-secret SIDs in +// ExtraData. It never stores the API Key Secret in ExtraData. func (d *Detector) Scan(_ context.Context, data []byte) []detector.RawFinding { - keyLocs := twilioKeyPattern.FindAllIndex(data, -1) - if len(keyLocs) == 0 { + secrets := assignmentMatches(twilioAPISecretAssignmentPattern, data) + if len(secrets) == 0 { return nil } - // Locate every Account SID once, then attach the nearest one to each key. - sidLocs := twilioSIDPattern.FindAllIndex(data, -1) + keySIDs := assignmentMatches(twilioKeySIDAssignmentPattern, data) + if len(keySIDs) == 0 { + return nil + } + accountSIDs := assignmentMatches(twilioAccountSIDAssignmentPattern, data) - findings := make([]detector.RawFinding, 0, len(keyLocs)) - for _, loc := range keyLocs { - match := data[loc[0]:loc[1]] - redacted := "SK****" + string(match[len(match)-4:]) - f := detector.RawFinding{ + findings := make([]detector.RawFinding, 0, len(secrets)) + usedKeySIDs := make([]bool, len(keySIDs)) + for _, secretMatch := range secrets { + secret := data[secretMatch.valueStart:secretMatch.valueEnd] + if isNonSecretValue(string(secret)) { + continue + } + + keyIndex, ambiguous := nearestUnusedCompanionMatch(data, secretMatch, keySIDs, usedKeySIDs) + if keyIndex < 0 { + continue + } + usedKeySIDs[keyIndex] = true + keyMatch := keySIDs[keyIndex] + + extra := map[string]string{ + "api_key_sid": string(data[keyMatch.valueStart:keyMatch.valueEnd]), + } + if ambiguous { + extra["pairing_ambiguous"] = "true" + } + if accountIndex := nearestCompanion(data, keyMatch, accountSIDs); accountIndex >= 0 { + accountMatch := accountSIDs[accountIndex] + extra["account_sid"] = string(data[accountMatch.valueStart:accountMatch.valueEnd]) + } + findings = append(findings, detector.RawFinding{ DetectorID: d.ID(), - Raw: bytes.Clone(match), - Redacted: redacted, + Raw: bytes.Clone(secret), + Redacted: detector.RedactBytes(secret), + ExtraData: extra, + ByteStart: secretMatch.valueStart, + ByteEnd: secretMatch.valueEnd, + }) + } + return findings +} + +type assignmentMatch struct { + wholeStart int + wholeEnd int + valueStart int + valueEnd int +} + +// assignmentMatches returns exact full-assignment and captured-value spans. +// Patterns may contain alternatives, so the first participating capture is +// used as the value. +func assignmentMatches(pattern *regexp.Regexp, data []byte) []assignmentMatch { + indices := pattern.FindAllSubmatchIndex(data, -1) + matches := make([]assignmentMatch, 0, len(indices)) + for _, index := range indices { + if len(index) < 4 { + continue } - if sid := nearestSID(data, loc, sidLocs); sid != "" { - f.ExtraData = map[string]string{ - "account_sid": sid, + valueStart, valueEnd := -1, -1 + for i := 2; i+1 < len(index); i += 2 { + if index[i] >= 0 && index[i+1] >= index[i] { + valueStart, valueEnd = index[i], index[i+1] + break } } - findings = append(findings, f) + if valueStart < 0 || valueEnd <= valueStart { + continue + } + if valueEnd < len(data) && (data[valueEnd] == '"' || data[valueEnd] == '\'') && escapedAt(data, valueEnd) { + // Do not treat an escaped quote inside a JSON, shell, or language + // string as the assignment terminator and report a truncated secret. + continue + } + wholeStart := index[0] + if wholeStart < len(data) && !isASCIIAlphaNumeric(data[wholeStart]) { + _, size := utf8.DecodeRune(data[wholeStart:]) + wholeStart += size + } + matches = append(matches, assignmentMatch{ + wholeStart: wholeStart, + // End correlation at the value, excluding a closing quote or a + // delimiter consumed only to prove the lexical boundary. + wholeEnd: valueEnd, + valueStart: valueStart, + valueEnd: valueEnd, + }) } - return findings + return matches +} + +func isASCIIAlphaNumeric(value byte) bool { + return value >= 'A' && value <= 'Z' || value >= 'a' && value <= 'z' || + value >= '0' && value <= '9' +} + +func escapedAt(data []byte, index int) bool { + backslashes := 0 + for i := index - 1; i >= 0 && data[i] == '\\'; i-- { + backslashes++ + } + return backslashes%2 == 1 +} + +func nearestUnusedCompanion(data []byte, target assignmentMatch, candidates []assignmentMatch, used []bool) int { + best, _ := nearestUnusedCompanionMatch(data, target, candidates, used) + return best } -// nearestSID returns the Account SID closest to the key match at keyLoc, but -// only when it lies within sidProximityWindow bytes. It returns an empty string -// when no SID is close enough, so a key with no nearby SID carries no -// (potentially wrong) account context. -func nearestSID(data []byte, keyLoc []int, sidLocs [][]int) string { +func nearestUnusedCompanionMatch(data []byte, target assignmentMatch, candidates []assignmentMatch, used []bool) (int, bool) { best := -1 bestDist := -1 - for i, sid := range sidLocs { - dist := rangeGap(keyLoc, sid) - if dist > sidProximityWindow { + ambiguous := false + for i, candidate := range candidates { + if used[i] { + continue + } + dist := rangeGap(target, candidate) + if dist > companionProximityWindow || !sameLogicalBlock(data, target, candidate) { continue } if bestDist == -1 || dist < bestDist { bestDist = dist best = i + ambiguous = false + } else if dist == bestDist { + ambiguous = true + // Consecutive SID/SECRET pairs put a secret equidistant from the + // preceding credential and the next SID. Prefer the preceding SID; + // suppressing the finding would hide a real credential. + if candidate.wholeEnd <= target.wholeStart && + (best < 0 || candidates[best].wholeEnd > target.wholeStart) { + best = i + } } } - if best == -1 { - return "" + return best, ambiguous +} + +func nearestCompanion(data []byte, target assignmentMatch, candidates []assignmentMatch) int { + return nearestUnusedCompanion(data, target, candidates, make([]bool, len(candidates))) +} + +func sameLogicalBlock(data []byte, a, b assignmentMatch) bool { + start, end := a.wholeEnd, b.wholeStart + if b.wholeEnd <= a.wholeStart { + start, end = b.wholeEnd, a.wholeStart } - return string(data[sidLocs[best][0]:sidLocs[best][1]]) + if start > end || start < 0 || end > len(data) { + return false + } + between := data[start:end] + return !logicalBlockSeparatorPattern.Match(between) && !bytes.ContainsAny(between, "{}") } -// rangeGap returns the number of bytes between two non-overlapping [start,end) -// ranges, or 0 when they touch or overlap. -func rangeGap(a, b []int) int { +// rangeGap returns the byte distance between two non-overlapping ranges. +func rangeGap(a, b assignmentMatch) int { switch { - case a[1] <= b[0]: - return b[0] - a[1] - case b[1] <= a[0]: - return a[0] - b[1] + case a.wholeEnd <= b.wholeStart: + return b.wholeStart - a.wholeEnd + case b.wholeEnd <= a.wholeStart: + return a.wholeStart - b.wholeEnd default: return 0 } } +func isNonSecretValue(value string) bool { + lower := strings.ToLower(strings.TrimSpace(value)) + if lower == "" { + return true + } + placeholders := map[string]struct{}{ + "change-me": {}, "change_me": {}, "changeme": {}, "dummy": {}, + "example": {}, "example-secret": {}, "example_secret": {}, "fixme": {}, + "foobar": {}, "not-a-real-secret": {}, "not_a_real_secret": {}, + "placeholder": {}, "redacted": {}, "replace-me": {}, "replace_me": {}, + "secret": {}, "string": {}, "test": {}, "todo": {}, + "api-key-secret": {}, "api_key_secret": {}, + "twilio-api-key-secret": {}, "twilio_api_key_secret": {}, "twilioapikeysecret": {}, + "your-api-key-secret": {}, "your_api_key_secret": {}, + "your-twilio-api-key-secret": {}, "your_twilio_api_key_secret": {}, "xxxxxxxx": {}, + } + if _, ok := placeholders[lower]; ok { + return true + } + for _, prefix := range []string{ + "${", "$", "{{", "%", "<", "vault://", "op://", "secret://", + "file://", "/run/secrets/", "@microsoft.keyvault", + } { + if strings.HasPrefix(lower, prefix) { + return true + } + } + if len(lower) >= 4 && strings.Trim(lower, "x*0-") == "" { + return true + } + return false +} + func init() { detector.Register(&Detector{}) } diff --git a/internal/detector/twilio/twilio_key_test.go b/internal/detector/twilio/twilio_key_test.go index b132086..d570387 100644 --- a/internal/detector/twilio/twilio_key_test.go +++ b/internal/detector/twilio/twilio_key_test.go @@ -2,6 +2,8 @@ package twilio import ( "context" + "fmt" + "regexp" "strings" "testing" @@ -11,163 +13,246 @@ import ( "github.com/stretchr/testify/require" ) +const ( + testKeySID = "SKabcdef0123456789abcdef0123456789" + testAccountSID = "AC1234567890abcdef1234567890ABCDEF" +) + +func syntheticSecret(seed string) string { + return strings.Repeat(seed, 4) +} + +func pairedFixture(secret string) string { + return "TWILIO_ACCOUNT_SID=" + testAccountSID + "\n" + + "TWILIO_API_KEY_SID=" + testKeySID + "\n" + + "TWILIO_API_KEY_SECRET=" + secret +} + func TestDetector_Metadata_ReturnsExpectedValues(t *testing.T) { d := &Detector{} assert.Equal(t, "twilio-api-key", d.ID()) - assert.Equal(t, "Twilio API Key", d.Description()) + assert.Equal(t, "Twilio API Key Secret", d.Description()) assert.Equal(t, finding.SeverityCritical, d.Severity()) - // The SK API-Key SID pattern is self-contained, so the detector declares no - // keywords and runs on every chunk (see Keywords doc comment). - assert.Empty(t, d.Keywords()) + assert.Equal(t, []string{"secret"}, d.Keywords()) + assert.True(t, d.AuthoritativeOnOverlap()) + contract := d.PlaygroundPatternContract() + require.Len(t, contract.Primary, 1) + require.Len(t, contract.RequiredNearby, 1) + assert.Equal(t, companionProximityWindow, contract.ProximityBytes) + assert.True(t, contract.SameLogicalBlock) + assert.True(t, contract.RejectPlaceholders) + assert.True(t, contract.OneToOne) } -func TestDetector_Scan_MatchesValidKeys(t *testing.T) { - // Synthetic 32-char hex string - hex32 := "abcdef0123456789abcdef0123456789" - hex32alt := "0123456789abcdef0123456789abcdef" +func TestDetector_Scan_ReportsPairedSecretAndNonSecretContext(t *testing.T) { + secret := syntheticSecret("Ab12Cd34") + findings := (&Detector{}).Scan(context.Background(), []byte(pairedFixture(secret))) + require.Len(t, findings, 1) + assert.Equal(t, []byte(secret), findings[0].Raw) + assert.Equal(t, "****"+secret[len(secret)-4:], findings[0].Redacted) + assert.Equal(t, testKeySID, findings[0].ExtraData["api_key_sid"]) + assert.Equal(t, testAccountSID, findings[0].ExtraData["account_sid"]) + assert.NotContains(t, findings[0].ExtraData, "api_key_secret") + assert.Equal(t, secret, pairedFixture(secret)[findings[0].ByteStart:findings[0].ByteEnd]) + for _, value := range findings[0].ExtraData { + assert.NotEqual(t, secret, value, "secret must never be copied into ExtraData") + } +} + +func TestDetector_Scan_SupportsExplicitRoleVariants(t *testing.T) { tests := []struct { - name string - input string - expected int - redacted string - extraSID string + input string + secret string }{ - { - name: "valid SK key with twilio context", - input: "twilio_api_key=SK" + hex32, - expected: 1, - redacted: "SK****" + hex32[len(hex32)-4:], - }, - { - name: "valid SK key with Account SID in same data", - input: "TWILIO_ACCOUNT_SID=AC" + hex32alt + "\nTWILIO_API_KEY=SK" + hex32, - expected: 1, - redacted: "SK****" + hex32[len(hex32)-4:], - extraSID: "AC" + hex32alt, - }, - { - name: "multiple SK keys in same input", - input: "twilio_key1=SK" + hex32 + "\ntwilio_key2=SK" + hex32alt, - expected: 2, - }, - { - name: "SK key embedded in config line", - input: `TWILIO_API_KEY="SK` + hex32 + `"`, - expected: 1, - redacted: "SK****" + hex32[len(hex32)-4:], - }, + {input: "TWILIO_API_KEY_SID=" + testKeySID + "\nTWILIO_API_SECRET=x7-K", secret: "x7-K"}, + {input: `{"twilioApiKeySid":"` + testKeySID + `","twilioApiKeySecret":"opaque/+value=="}`, secret: "opaque/+value=="}, + {input: "twilio-api-key-sid=" + testKeySID + "\ntwilio-api-key-secret='Secret.with.punctuation_42'", secret: "Secret.with.punctuation_42"}, + {input: `{"Twilio":{"ApiKeySid":"` + testKeySID + `","ApiKeySecret":"` + syntheticSecret("Qw12Er34") + `"}}`, secret: syntheticSecret("Qw12Er34")}, + {input: "TWILIO_API_KEY_SID=" + testKeySID + "\nTWILIO_API_KEY_SECRET=real-example-secret-value-42", secret: "real-example-secret-value-42"}, + {input: "MYAPP_TWILIO_API_KEY_SID=" + testKeySID + "\nMYAPP_TWILIO_API_KEY_SECRET=" + syntheticSecret("Mn12Bv34"), secret: syntheticSecret("Mn12Bv34")}, + {input: "PROD_TWILIO_API_KEY_SID=" + testKeySID + "\nPROD_TWILIO_API_KEY_SECRET=" + syntheticSecret("Rt56Yu78"), secret: syntheticSecret("Rt56Yu78")}, + {input: "X_API_KEY_SID=" + testKeySID + "\nX_API_KEY_SECRET=" + syntheticSecret("Kp90Lm12"), secret: syntheticSecret("Kp90Lm12")}, + {input: "environment:\n - TWILIO_API_KEY_SID=" + testKeySID + "\n - TWILIO_API_KEY_SECRET=" + syntheticSecret("Dc34Fv56"), secret: syntheticSecret("Dc34Fv56")}, } + for i, test := range tests { + findings := (&Detector{}).Scan(context.Background(), []byte(test.input)) + require.Lenf(t, findings, 1, "variant %d", i) + assert.Equal(t, []byte(test.secret), findings[0].Raw) + assert.Equal(t, test.secret, test.input[findings[0].ByteStart:findings[0].ByteEnd]) + } +} - d := &Detector{} - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - findings := d.Scan(context.Background(), []byte(tt.input)) - assert.Len(t, findings, tt.expected) - if tt.expected > 0 && tt.redacted != "" { - require.NotEmpty(t, findings) - assert.Equal(t, tt.redacted, findings[0].Redacted) - assert.Len(t, findings[0].Raw, 34) // "SK" + 32 hex chars - } - if tt.extraSID != "" { - require.NotEmpty(t, findings) - require.NotNil(t, findings[0].ExtraData) - assert.Equal(t, tt.extraSID, findings[0].ExtraData["account_sid"]) - } +func TestDetector_ScanViaMatcher_PairedSecretIsDetected(t *testing.T) { + secret := syntheticSecret("Zx98Cv76") + findings := testutil.ScanViaMatcher(&Detector{}, []byte(pairedFixture(secret))) + + require.Len(t, findings, 1) + assert.Equal(t, []byte(secret), findings[0].Raw) +} + +func TestDetector_Scan_DoesNotReportBareIdentifiersOrUnpairedValues(t *testing.T) { + secret := syntheticSecret("Ab12Cd34") + tests := map[string]string{ + "bare API Key SID": testKeySID, + "bare Account SID": testAccountSID, + "labelled secret without SID": "TWILIO_API_KEY_SECRET=" + secret, + "bare SID beside secret": testKeySID + "\nTWILIO_API_KEY_SECRET=" + secret, + "SID under unrelated role": "ID=" + testKeySID + "\nTWILIO_API_KEY_SECRET=" + secret, + "SID with generic password": "TWILIO_API_KEY_SID=" + testKeySID + "\nPASSWORD=" + secret, + "other provider secret role": "TWILIO_API_KEY_SID=" + testKeySID + "\nSTRIPE_API_SECRET=" + secret, + "ordinary placeholder": "TWILIO_API_KEY_SID=" + testKeySID + "\nTWILIO_API_KEY_SECRET=your_api_key_secret", + "canonical Twilio placeholder": "TWILIO_API_KEY_SID=" + testKeySID + "\nTWILIO_API_KEY_SECRET=YOUR_TWILIO_API_KEY_SECRET", + "external secret reference": "TWILIO_API_KEY_SID=" + testKeySID + "\nTWILIO_API_KEY_SECRET=${TWILIO_API_KEY_SECRET}", + "separate logical block": "TWILIO_API_KEY_SID=" + testKeySID + "\n\nTWILIO_API_KEY_SECRET=" + secret, + "separate JSON objects": `{"ApiKeySid":"` + testKeySID + `"},{"ApiKeySecret":"` + secret + `"}`, + "role embedded in identifier": "TWILIO_API_KEY_SID=" + testKeySID + "\nNOTWILIO_API_KEY_SECRET=" + secret, + "escaped quote is not truncated": `{"ApiKeySid":"` + testKeySID + `","ApiKeySecret":"opaque\"tail"}`, + "overlong unquoted secret": "TWILIO_API_KEY_SID=" + testKeySID + "\nTWILIO_API_KEY_SECRET=" + strings.Repeat("a", 513), + "hyphen suffixed SID": "TWILIO_API_KEY_SID=" + testKeySID + "-not-a-real-sid\nTWILIO_API_KEY_SECRET=" + secret, + "dot suffixed SID": "TWILIO_API_KEY_SID=" + testKeySID + ".suffix\nTWILIO_API_KEY_SECRET=" + secret, + "quoted suffixed SID": `{"ApiKeySid":"` + testKeySID + `-suffix","ApiKeySecret":"` + secret + `"}`, + } + for name, input := range tests { + t.Run(name, func(t *testing.T) { + assert.Empty(t, (&Detector{}).Scan(context.Background(), []byte(input))) }) } } -// TestDetector_ScanViaMatcher_StandaloneKey_IsDetected is a regression test for -// the keyword/regex misalignment (DETB-M-01). A standalone SK API-Key SID -// carries none of the words "twilio", so it must not be gated out by the -// Aho-Corasick matcher before Scan runs. -func TestDetector_ScanViaMatcher_StandaloneKey_IsDetected(t *testing.T) { - hex32 := "abcdef0123456789abcdef0123456789" - key := "SK" + hex32 +func TestDetector_ScanViaMatcher_RejectsTemplatesAndMalformedSID(t *testing.T) { + secret := syntheticSecret("Ab12Cd34") + for name, input := range map[string]string{ + "uppercase canonical placeholder": "TWILIO_API_KEY_SID=" + testKeySID + "\nTWILIO_API_KEY_SECRET=YOUR_TWILIO_API_KEY_SECRET", + "lowercase canonical placeholder": "TWILIO_API_KEY_SID=" + testKeySID + "\nTWILIO_API_KEY_SECRET=your_twilio_api_key_secret", + "hyphen suffixed SID": "TWILIO_API_KEY_SID=" + testKeySID + "-suffix\nTWILIO_API_KEY_SECRET=" + secret, + "dot suffixed SID": "TWILIO_API_KEY_SID=" + testKeySID + ".suffix\nTWILIO_API_KEY_SECRET=" + secret, + } { + t.Run(name, func(t *testing.T) { + assert.Empty(t, testutil.ScanViaMatcher(&Detector{}, []byte(input))) + }) + } +} - d := &Detector{} - findings := testutil.ScanViaMatcher(d, []byte(key)) +func TestDetector_Scan_CompanionOutsideWindowIsNotPaired(t *testing.T) { + secret := syntheticSecret("Ab12Cd34") + input := "TWILIO_API_KEY_SID=" + testKeySID + strings.Repeat(" ", companionProximityWindow+128) + + "TWILIO_API_KEY_SECRET=" + secret - require.Len(t, findings, 1, "standalone SK key must survive the matcher gate") - assert.Equal(t, "twilio-api-key", findings[0].DetectorID) - assert.Equal(t, "SK****"+hex32[len(hex32)-4:], findings[0].Redacted) + assert.Empty(t, (&Detector{}).Scan(context.Background(), []byte(input))) } -// TestDetector_Scan_ScopesAccountSIDToNearestKey verifies that when two -// unrelated key/SID pairs appear in the same chunk, each key is attributed the -// SID nearest to it rather than the first SID in the chunk. -func TestDetector_Scan_ScopesAccountSIDToNearestKey(t *testing.T) { - hexA := "aaaaaaaa11111111aaaaaaaa11111111" - hexB := "bbbbbbbb22222222bbbbbbbb22222222" - sidA := "AC" + hexA - sidB := "AC" + hexB - keyA := "SK" + hexA - keyB := "SK" + hexB - - // Two well-separated credential blocks: block A then, far away, block B. - input := sidA + " " + keyA + strings.Repeat(" ", 600) + sidB + " " + keyB +func TestDetector_Scan_ProximityUsesUTF8Bytes(t *testing.T) { + secret := "opaque-real-value-42" + tests := []struct { + name string + gap string + want int + }{ + {name: "ASCII 512", gap: "\n" + strings.Repeat("a", 510) + " ", want: 1}, + {name: "ASCII 513", gap: "\n" + strings.Repeat("a", 511) + " ", want: 0}, + {name: "two-byte Unicode 512", gap: "\n" + strings.Repeat("é", 255) + " ", want: 1}, + {name: "two-byte Unicode over limit", gap: "\n" + strings.Repeat("é", 256) + " ", want: 0}, + {name: "astral Unicode 512", gap: "\n" + strings.Repeat("🔐", 127) + " ", want: 1}, + {name: "astral Unicode 513", gap: "\n" + strings.Repeat("🔐", 127) + " ", want: 0}, + {name: "CRLF 512", gap: "\r\n" + strings.Repeat("a", 509) + " ", want: 1}, + {name: "CRLF 513", gap: "\r\n" + strings.Repeat("a", 510) + " ", want: 0}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + input := "TWILIO_API_KEY_SID=" + testKeySID + test.gap + "TWILIO_API_KEY_SECRET=" + secret + assert.Len(t, (&Detector{}).Scan(context.Background(), []byte(input)), test.want) + }) + } +} - d := &Detector{} - findings := d.Scan(context.Background(), []byte(input)) +func TestDetector_Scan_AssociatesNearestKeySID(t *testing.T) { + secretA := syntheticSecret("Aa11Bb22") + secretB := syntheticSecret("Cc33Dd44") + keyA := "SKaaaaaaaa11111111aaaaaaaa11111111" + keyB := "SKbbbbbbbb22222222bbbbbbbb22222222" + input := "TWILIO_API_KEY_SID=" + keyA + "\nTWILIO_API_KEY_SECRET=" + secretA + + strings.Repeat(" ", companionProximityWindow+128) + + "TWILIO_API_KEY_SID=" + keyB + "\nTWILIO_API_KEY_SECRET=" + secretB + findings := (&Detector{}).Scan(context.Background(), []byte(input)) require.Len(t, findings, 2) - require.NotNil(t, findings[0].ExtraData) - require.NotNil(t, findings[1].ExtraData) - assert.Equal(t, sidA, findings[0].ExtraData["account_sid"]) - assert.Equal(t, sidB, findings[1].ExtraData["account_sid"]) + assert.Equal(t, keyA, findings[0].ExtraData["api_key_sid"]) + assert.Equal(t, keyB, findings[1].ExtraData["api_key_sid"]) } -// TestDetector_Scan_FarAwaySID_NotAttributed verifies that an Account SID -// beyond the proximity window is not attached to a key. -func TestDetector_Scan_FarAwaySID_NotAttributed(t *testing.T) { - hex32 := "abcdef0123456789abcdef0123456789" - // SID sits well beyond sidProximityWindow (512) bytes from the key. - input := "AC" + strings.Repeat("a", 32) + strings.Repeat(" ", 700) + "SK" + hex32 +func TestDetector_Scan_ConsecutivePairsAreNotSuppressedOnTies(t *testing.T) { + var input strings.Builder + wantSIDs := make([]string, 0, 5) + for i := range 5 { + keySID := fmt.Sprintf("SK%032x", i+1) + secret := fmt.Sprintf("opaque-secret-%d-Q7mN2pL9rT4vW8xY", i+1) + wantSIDs = append(wantSIDs, keySID) + fmt.Fprintf(&input, "TWILIO_API_KEY_SID=%s\nTWILIO_API_KEY_SECRET=%s\n", keySID, secret) + } + + findings := testutil.ScanViaMatcher(&Detector{}, []byte(input.String())) + require.Len(t, findings, len(wantSIDs)) + for i, got := range findings { + assert.Equal(t, wantSIDs[i], got.ExtraData["api_key_sid"]) + } +} - d := &Detector{} - findings := d.Scan(context.Background(), []byte(input)) +func TestDetector_Scan_DoesNotReuseOneSIDForMultipleSecrets(t *testing.T) { + input := "TWILIO_API_KEY_SID=" + testKeySID + "\n" + + "TWILIO_API_KEY_SECRET=first-opaque-secret\n" + + "TWILIO_API_KEY_SECRET=second-opaque-secret" + findings := (&Detector{}).Scan(context.Background(), []byte(input)) require.Len(t, findings, 1) - assert.Nil(t, findings[0].ExtraData) + assert.Equal(t, []byte("first-opaque-secret"), findings[0].Raw) } -func TestDetector_Scan_RejectsInvalidInput(t *testing.T) { - tests := []struct { - name string - input string - }{ - { - name: "wrong prefix with twilio context", - input: "twilio_key=AB" + strings.Repeat("a", 32), - }, - { - name: "too short hex with twilio context", - input: "twilio_key=SKabcdef0123", - }, - { - name: "uppercase hex rejected", - input: "twilio_key=SK" + strings.ToUpper(strings.Repeat("ab", 16)), - }, - { - name: "Account SID alone is not a finding", - input: "twilio_sid=AC" + strings.Repeat("a", 32), - }, - { - name: "plain text", - input: "this is just normal text", - }, - { - name: "empty input", - input: "", - }, +func TestDetector_Scan_ExactSpanSelectsAcceptedOccurrence(t *testing.T) { + secret := syntheticSecret("Aa12Bb34") + input := "NOTE=" + secret + " # rejected duplicate\n" + pairedFixture(secret) + + findings := (&Detector{}).Scan(context.Background(), []byte(input)) + require.Len(t, findings, 1) + assert.Equal(t, strings.LastIndex(input, secret), findings[0].ByteStart) + assert.Equal(t, secret, input[findings[0].ByteStart:findings[0].ByteEnd]) +} + +func TestAssignmentMatches_FailsClosedOnMissingOrEmptyCapture(t *testing.T) { + assert.Empty(t, assignmentMatches(regexp.MustCompile(`x`), []byte("x"))) + assert.Empty(t, assignmentMatches(regexp.MustCompile(`x(y)?`), []byte("x"))) + assert.Empty(t, assignmentMatches(regexp.MustCompile(`x()`), []byte("x"))) + + matches := assignmentMatches(regexp.MustCompile(`x(y)`), []byte("xy")) + require.Len(t, matches, 1) + assert.Equal(t, assignmentMatch{wholeStart: 0, wholeEnd: 2, valueStart: 1, valueEnd: 2}, matches[0]) + + assert.Empty(t, assignmentMatches(twilioKeySIDAssignmentPattern, []byte("TWILIO_API_KEY_SID="+testKeySID+"-suffix"))) + assert.Empty(t, assignmentMatches(twilioAccountSIDAssignmentPattern, []byte("TWILIO_ACCOUNT_SID="+testAccountSID+".suffix"))) +} + +func TestCompanionSelection_PrefersPrecedingOnTieAndRejectsInvalidBlocks(t *testing.T) { + data := []byte(strings.Repeat(" ", 200)) + target := assignmentMatch{wholeStart: 100, wholeEnd: 110, valueStart: 105, valueEnd: 110} + candidates := []assignmentMatch{ + {wholeStart: 80, wholeEnd: 90, valueStart: 80, valueEnd: 90}, + {wholeStart: 120, wholeEnd: 130, valueStart: 120, valueEnd: 130}, } + assert.Equal(t, 0, nearestUnusedCompanion(data, target, candidates, []bool{false, false})) + assert.Equal(t, 1, nearestUnusedCompanion(data, target, candidates, []bool{true, false})) - d := &Detector{} - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - findings := d.Scan(context.Background(), []byte(tt.input)) - assert.Empty(t, findings) - }) + assert.False(t, sameLogicalBlock(data, assignmentMatch{wholeStart: -1, wholeEnd: -1}, target)) + assert.Equal(t, 0, rangeGap(target, assignmentMatch{wholeStart: 105, wholeEnd: 115})) +} + +func TestIsNonSecretValue_ReferenceAndPlaceholderMatrix(t *testing.T) { + for _, value := range []string{ + "change-me", "NOT_A_REAL_SECRET", "replace_me", "${SECRET}", "$SECRET", + "{{ secret }}", "%SECRET%", "", "vault://path", "op://vault/item", + "secret://path", "file://path", "/run/secrets/token", "@Microsoft.KeyVault(...)集", + "****", "0000", "----", + } { + assert.Truef(t, isNonSecretValue(value), "%q", value) } + assert.False(t, isNonSecretValue("opaque-real-value-42")) } diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 5fcbd08..a341961 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -48,10 +48,13 @@ type Config struct { // to be reported. When EnableEntropy is set the engine computes // Finding.Entropy for every match (informational), but only drops a finding // when its detector opts into the gate via EntropyBased() — i.e. the generic - // API-key detector and other high-entropy-string heuristics. Structural - // detectors (AWS, GitHub, Stripe, …) are never entropy-gated, so a valid but - // low-entropy key is always reported. When EnableEntropy is false no entropy - // is computed and no gating is applied. Defaulted to defaultEntropyThreshold. + // API-key detector and other high-entropy-string heuristics. A mixed detector + // may refine that decision per finding via EntropyGated(raw), preserving the + // gate for heuristic matches while exempting strong structural context. + // Structural detectors (AWS, GitHub, Stripe, …) are never entropy-gated, so a + // valid but low-entropy key is always reported. When EnableEntropy is false no + // entropy is computed and no gating is applied. Defaulted to + // defaultEntropyThreshold. EntropyThreshold float64 ShowRaw bool Clock func() time.Time // Optional; defaults to time.Now @@ -106,20 +109,50 @@ type entropyBased interface { EntropyBased() bool } +// findingEntropyGate is an optional refinement for mixed detectors that emit +// both heuristic and structurally contextual findings. It lets the detector +// retain the global entropy gate for heuristic matches while exempting only a +// specific raw finding backed by strong structure. +type findingEntropyGate interface { + EntropyGated(raw detector.RawFinding) bool +} + +type locatedVerifyPair struct { + pair verifier.VerifyPair + start, end int + isFallback bool + isAuthority bool +} + // isEntropyBased reports whether the detector opts into the engine entropy gate. func isEntropyBased(d detector.Detector) bool { eb, ok := d.(entropyBased) return ok && eb.EntropyBased() } +func isEntropyGated(d detector.Detector, raw detector.RawFinding) bool { + if !isEntropyBased(d) { + return false + } + if policy, ok := d.(findingEntropyGate); ok { + return policy.EntropyGated(raw) + } + return true +} + // Scan scans the given source and returns results. func (e *Engine) Scan(ctx context.Context, src source.Source) (*ScanResult, error) { - if err := src.Validate(); err != nil { + start := time.Now() + if err := ctx.Err(); err != nil { + return interruptedResult(start), err + } + if err := src.Validate(ctx); err != nil { + if ctx.Err() != nil { + return interruptedResult(start), ctx.Err() + } return nil, fmt.Errorf("source validation failed: %w", err) } - start := time.Now() - slog.Info( "scan started", "source", src.Type(), @@ -129,136 +162,116 @@ func (e *Engine) Scan(ctx context.Context, src source.Source) (*ScanResult, erro jobs := make(chan source.Chunk, e.config.Concurrency*channelBufferMultiplier) results := make(chan verifier.VerifyPair, e.config.Concurrency*channelBufferMultiplier) + workers := e.startScanWorkers(ctx, jobs, results) + collected := e.startResultCollector(ctx, results) + scannedChunks, fullyDrained := enqueueSourceChunks(ctx, src, jobs) + close(jobs) + srcErr := completedSourceError(src, fullyDrained, scannedChunks) + workers.Wait() + close(results) + findings := e.applyFilters(<-collected) + sortFindings(findings) + result := completedScanResult(start, findings, scannedChunks, ctx.Err() != nil) + + if ctx.Err() != nil { + return result, fmt.Errorf("scan interrupted: %w", ctx.Err()) + } - // Start workers. - var wg sync.WaitGroup - for i := 0; i < e.config.Concurrency; i++ { - wg.Add(1) + // A terminal source failure must not be reported as a clean, empty scan: the + // engine surfaces it as an error even though a (possibly partial) result is + // still returned alongside it. + if srcErr != nil { + return result, fmt.Errorf("scan source failed: %w", srcErr) + } + + return result, nil +} + +func (e *Engine) startScanWorkers( + ctx context.Context, + jobs <-chan source.Chunk, + results chan<- verifier.VerifyPair, +) *sync.WaitGroup { + workers := &sync.WaitGroup{} + for range e.config.Concurrency { + workers.Add(1) go func() { - defer wg.Done() + defer workers.Done() e.worker(ctx, jobs, results) }() } + return workers +} - // Collect and verify results incrementally (bounded streaming). - // - // Raw secret byte lifetime: each VerifyPair carries the raw secret bytes - // (p.Raw) in memory only so that verification can re-present them to the - // relevant API. These bytes are never logged, written to disk, or otherwise - // persisted (per the project's secret-safety rule). - // - // Memory bound (ENG-M-02, resolved): rather than accumulating every detected - // pair for the whole scan before verifying, the collector buffers at most - // maxPairsInFlight pairs, streams that batch into verification, appends the - // resulting findings, and drops its references to the raw bytes so they - // become garbage-collectable immediately. Peak raw-secret memory is therefore - // bounded by maxPairsInFlight rather than by the total number of findings. - var findings []finding.Finding - var collectWg sync.WaitGroup - collectWg.Add(1) +// startResultCollector keeps raw-secret memory bounded to maxPairsInFlight and +// clears every processed batch before accepting the next one. +func (e *Engine) startResultCollector(ctx context.Context, results <-chan verifier.VerifyPair) <-chan []finding.Finding { + collected := make(chan []finding.Finding, 1) go func() { - defer collectWg.Done() + var findings []finding.Finding batch := make([]verifier.VerifyPair, 0, maxPairsInFlight) - flush := func() { - if len(batch) == 0 { - return - } - findings = append(findings, e.verifyBatch(ctx, batch)...) - // Drop references to raw secret bytes so they are GC-eligible now - // instead of living until the whole scan completes. - for i := range batch { - batch[i] = verifier.VerifyPair{} - } - batch = batch[:0] - } - for p := range results { - batch = append(batch, p) + for pair := range results { + batch = append(batch, pair) if len(batch) >= maxPairsInFlight { - flush() + findings = append(findings, e.verifyBatch(ctx, batch)...) + batch = clearVerificationBatch(batch) } } - flush() + findings = append(findings, e.verifyBatch(ctx, batch)...) + clearVerificationBatch(batch) + collected <- findings + close(collected) }() + return collected +} + +func clearVerificationBatch(batch []verifier.VerifyPair) []verifier.VerifyPair { + for index := range batch { + batch[index] = verifier.VerifyPair{} + } + return batch[:0] +} - // Send chunks to the jobs channel. - // NOTE: Context cancellation during this loop depends on the source implementation - // closing its Chunks channel promptly when ctx is cancelled. If a source blocks - // indefinitely on send, this loop may not exit until the source returns. - scannedChunks := 0 - // fullyDrained is true only when the range below exits because the source - // closed its chunks channel (normal completion), false when we break out - // early on cancellation. It gates the src.Err() read below: reading the - // source's terminal error is only race-free once its channel is closed. - fullyDrained := true -loop: +func enqueueSourceChunks(ctx context.Context, src source.Source, jobs chan<- source.Chunk) (int, bool) { + scanned := 0 for chunk := range src.Chunks(ctx) { select { case <-ctx.Done(): - fullyDrained = false - break loop + return scanned, false case jobs <- chunk: - scannedChunks++ + scanned++ } } - close(jobs) + return scanned, true +} - // Surface the source's terminal outcome. src.Err() is only valid to read - // after the source's chunks channel is fully drained (closed); that close is - // the happens-before edge publishing the error the source recorded before - // closing. On the cancellation break path the source goroutine may still be - // running, so we skip the read and let the interrupt path below report the - // outcome instead. - var srcErr error - if fullyDrained { - srcErr = src.Err() - if srcErr == nil && scannedChunks == 0 { - // A clean scan that produced nothing: make the silently-empty result - // visible so "0 findings, nothing scanned" is not mistaken for a - // verified-clean target. - slog.Warn("scan target yielded no scannable content", "source", src.Type()) - } +func completedSourceError(src source.Source, fullyDrained bool, scannedChunks int) error { + if !fullyDrained { + return nil } + err := src.Err() + if err == nil && scannedChunks == 0 { + slog.Warn("scan target yielded no scannable content", "source", src.Type()) + } + return err +} - // Wait for workers to finish - wg.Wait() - close(results) - - // Wait for the collector (which also ran verification) to finish. - collectWg.Wait() - - // Apply post-scan filters. - findings = e.applyFilters(findings) - - // Impose a single deterministic order so output is stable run-to-run. - sortFindings(findings) - +func completedScanResult(start time.Time, findings []finding.Finding, scannedChunks int, interrupted bool) *ScanResult { result := &ScanResult{ - Findings: findings, - ScannedChunks: scannedChunks, - Duration: time.Since(start), - Interrupted: ctx.Err() != nil, + Findings: findings, ScannedChunks: scannedChunks, + Duration: time.Since(start), Interrupted: interrupted, } + slog.Info("scan completed", "findings", len(findings), "chunks", scannedChunks, + "duration", result.Duration, "interrupted", result.Interrupted) + return result +} - slog.Info( - "scan completed", - "findings", len(findings), - "chunks", scannedChunks, - "duration", result.Duration, - "interrupted", result.Interrupted, - ) - - if ctx.Err() != nil { - return result, fmt.Errorf("scan interrupted: %w", ctx.Err()) +func interruptedResult(start time.Time) *ScanResult { + return &ScanResult{ + Findings: []finding.Finding{}, + Duration: time.Since(start), + Interrupted: true, } - - // A terminal source failure must not be reported as a clean, empty scan: the - // engine surfaces it as an error even though a (possibly partial) result is - // still returned alongside it. - if srcErr != nil { - return result, fmt.Errorf("scan source failed: %w", srcErr) - } - - return result, nil } // verifyBatch verifies a bounded batch of collected pairs and returns the @@ -326,6 +339,15 @@ func (e *Engine) worker(ctx context.Context, jobs <-chan source.Chunk, results c // Aho-Corasick pre-filtering: only run matched detectors. matchedDetectors := e.matcher.Match(chunk.Data) + // Broad fallback detectors run first and buffer only their bounded result + // set. Other findings can then stream immediately; only explicitly + // authoritative provider findings may remove overlapping fallbacks. This + // preserves the bounded raw-secret memory model instead of accumulating + // every provider finding in a chunk. + sort.SliceStable(matchedDetectors, func(i, j int) bool { + return detectorIsOverlapFallback(matchedDetectors[i]) && !detectorIsOverlapFallback(matchedDetectors[j]) + }) + fallbackPairs := make([]locatedVerifyPair, 0, 8) for _, det := range matchedDetectors { select { @@ -343,10 +365,9 @@ func (e *Engine) worker(ctx context.Context, jobs <-chan source.Chunk, results c rawFindings := det.Scan(ctx, chunk.Data) // Track the search position per raw value so repeated matches of the - // same bytes resolve to distinct offsets. Detectors emit findings in - // left-to-right match order (regexp.FindAll guarantees this; this - // engine-level assumption is documented here because the Detector - // interface does not encode it), so the Nth occurrence of a raw value + // same bytes resolve to distinct offsets. The Detector interface requires + // stable left-to-right output (regexp.FindAll naturally guarantees it), + // so the Nth occurrence of a raw value // maps to its Nth position in the chunk. The cursor map is allocated // only when a detector returns more than one finding — the common // zero/one-finding case uses a plain index scan. @@ -356,12 +377,7 @@ func (e *Engine) worker(ctx context.Context, jobs <-chan source.Chunk, results c } for _, raw := range rawFindings { - var offset int - if offsetCursor != nil { - offset = nextMatchOffset(chunk.Data, raw.Raw, offsetCursor) - } else { - offset = firstMatchOffset(chunk.Data, raw.Raw) - } + offset := rawFindingOffset(chunk.Data, raw, offsetCursor) // Resolve the 1-based line number and its text in a single pass; // both the ID/line derivation and the inline-ignore check reuse it @@ -372,12 +388,12 @@ func (e *Engine) worker(ctx context.Context, jobs <-chan source.Chunk, results c // Entropy gating: the Shannon-entropy floor applies ONLY to // heuristic detectors that opt in via EntropyBased() (e.g. the - // generic API-key detector). Structural detectors — AWS, GitHub, - // Stripe, etc. — have precise formats and are never dropped on - // entropy, so a valid but low-entropy key (an AWS access-key ID - // is ~3.9 bits) is always reported. + // generic API-key detector), subject to an optional per-finding + // EntropyGated refinement for mixed structural/heuristic detectors. + // Structural findings — AWS, GitHub, Stripe, explicit provider + // header contexts, etc. — are never dropped on entropy. if e.config.EnableEntropy && len(raw.Raw) > 0 && - isEntropyBased(det) && f.Entropy < e.config.EntropyThreshold { + isEntropyGated(det, raw) && f.Entropy < e.config.EntropyThreshold { continue } @@ -388,15 +404,97 @@ func (e *Engine) worker(ctx context.Context, jobs <-chan source.Chunk, results c continue } - pair := verifier.VerifyPair{Finding: f, Raw: raw} + candidate := locatedVerifyPair{ + pair: verifier.VerifyPair{Finding: f, Raw: raw}, + start: offset, + end: rawFindingEnd(chunk.Data, raw, offset), + isFallback: detectorIsOverlapFallback(det), + isAuthority: detectorIsOverlapAuthority(det), + } + if candidate.isFallback { + fallbackPairs = append(fallbackPairs, candidate) + continue + } + + fallbackPairs = suppressFallbacksForSpecialized(fallbackPairs, candidate) select { case <-ctx.Done(): return - case results <- pair: + case results <- candidate.pair: } } } + + for _, candidate := range fallbackPairs { + select { + case <-ctx.Done(): + return + case results <- candidate.pair: + } + } + } +} + +func detectorIsOverlapFallback(det detector.Detector) bool { + fallback, ok := det.(detector.OverlapFallback) + return ok && fallback.FallbackOnSpecializedOverlap() +} + +func detectorIsOverlapAuthority(det detector.Detector) bool { + authority, ok := det.(detector.OverlapAuthority) + return ok && authority.AuthoritativeOnOverlap() +} + +// suppressFallbacksForSpecialized drops a buffered broad-context finding only +// when the newly produced authoritative, equal-or-higher-severity finding +// intersects its source bytes. Provider-provider overlaps and repeated values +// at distinct locations remain separate findings. +func suppressFallbacksForSpecialized( + fallbacks []locatedVerifyPair, + specialized locatedVerifyPair, +) []locatedVerifyPair { + if len(fallbacks) == 0 || !specialized.isAuthority || + specialized.start < 0 || specialized.end <= specialized.start { + return fallbacks + } + result := fallbacks[:0] + for _, candidate := range fallbacks { + if candidate.start < 0 || candidate.end <= candidate.start { + result = append(result, candidate) + continue + } + if specialized.pair.Finding.Severity < candidate.pair.Finding.Severity || + candidate.start >= specialized.end || specialized.start >= candidate.end { + result = append(result, candidate) + } + } + return result +} + +// rawFindingOffset returns a detector-provided exact byte span when it is +// internally consistent, otherwise it preserves the legacy raw-value search. +// Exact spans prevent a same-valued decoy earlier in the chunk from stealing a +// finding's line number or inline-ignore decision. +func rawFindingOffset(data []byte, raw detector.RawFinding, cursor map[string]int) int { + if raw.ByteEnd > raw.ByteStart && raw.ByteStart >= 0 && raw.ByteEnd <= len(data) && + bytes.Equal(data[raw.ByteStart:raw.ByteEnd], raw.Raw) { + return raw.ByteStart + } + if cursor != nil { + return nextMatchOffset(data, raw.Raw, cursor) + } + return firstMatchOffset(data, raw.Raw) +} + +func rawFindingEnd(data []byte, raw detector.RawFinding, offset int) int { + if raw.ByteEnd > raw.ByteStart && raw.ByteStart == offset && raw.ByteEnd <= len(data) && + bytes.Equal(data[raw.ByteStart:raw.ByteEnd], raw.Raw) { + return raw.ByteEnd + } + if offset < 0 || len(raw.Raw) == 0 || offset > len(data)-len(raw.Raw) { + return -1 } + return offset + len(raw.Raw) } // firstMatchOffset returns the byte offset of the first occurrence of raw in @@ -532,7 +630,7 @@ func (e *Engine) rawToFinding(raw detector.RawFinding, chunk source.Chunk, det d } if e.config.EnableEntropy && len(raw.Raw) > 0 { - f.Entropy = entropy.Calculate(raw.Raw) + f.SetEntropy(entropy.Calculate(raw.Raw)) } // Deterministic ID: detectorID + redacted + filePath + line + offset. diff --git a/internal/engine/engine_test.go b/internal/engine/engine_test.go index ed2a3c3..d8a0a67 100644 --- a/internal/engine/engine_test.go +++ b/internal/engine/engine_test.go @@ -27,8 +27,8 @@ type mockSource struct { err error } -func (m *mockSource) Type() string { return "mock" } -func (m *mockSource) Validate() error { return nil } +func (m *mockSource) Type() string { return "mock" } +func (m *mockSource) Validate(context.Context) error { return nil } func (m *mockSource) Chunks(_ context.Context) <-chan source.Chunk { ch := make(chan source.Chunk, len(m.chunks)) for _, c := range m.chunks { @@ -59,6 +59,43 @@ func (m *mockDetector) Severity() finding.Severity { func (m *mockDetector) Scan(_ context.Context, _ []byte) []detector.RawFinding { return m.findings } func (m *mockDetector) EntropyBased() bool { return m.entropyBased } +type mixedEntropyDetector struct { + *mockDetector + gateFinding bool +} + +type fallbackMockDetector struct { + *mockDetector +} + +func (d *fallbackMockDetector) FallbackOnSpecializedOverlap() bool { return true } + +type authorityMockDetector struct { + *mockDetector +} + +func (d *authorityMockDetector) AuthoritativeOnOverlap() bool { return true } + +func (m *mixedEntropyDetector) EntropyGated(_ detector.RawFinding) bool { + return m.gateFinding +} + +// policyOnlyDetector intentionally has EntropyGated but not EntropyBased. It +// proves that a coincidentally named method cannot opt a structural detector +// into the entropy floor. +type policyOnlyDetector struct { + findings []detector.RawFinding +} + +func (d *policyOnlyDetector) ID() string { return "policy-only" } +func (d *policyOnlyDetector) Description() string { return "policy only" } +func (d *policyOnlyDetector) Keywords() []string { return nil } +func (d *policyOnlyDetector) Severity() finding.Severity { return finding.SeverityMedium } +func (d *policyOnlyDetector) EntropyGated(detector.RawFinding) bool { return true } +func (d *policyOnlyDetector) Scan(_ context.Context, _ []byte) []detector.RawFinding { + return d.findings +} + var fixedClock = func() time.Time { return time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) } // --- Tests --- @@ -95,6 +132,157 @@ func TestScan_SingleChunkSingleDetector_ReturnsOneFinding(t *testing.T) { assert.False(t, result.Interrupted) } +func TestScan_SpecializedOverlapSuppressesOnlyFallbackFinding(t *testing.T) { + data := []byte(`{"AccessToken":"shpat_0123456789abcdef0123456789abcdef"}`) + wholeStart := bytes.Index(data, []byte("shpat_")) + wholeEnd := wholeStart + len("shpat_0123456789abcdef0123456789abcdef") + + fallback := &fallbackMockDetector{mockDetector: &mockDetector{ + id: "structured-fallback", + severity: finding.SeverityHigh, + findings: []detector.RawFinding{{ + DetectorID: "structured-fallback", + Raw: data[wholeStart:wholeEnd], + Redacted: "fallback-redacted", + ByteStart: wholeStart, + ByteEnd: wholeEnd, + }}, + }} + specialized := &authorityMockDetector{mockDetector: &mockDetector{ + id: "provider-specialized", + severity: finding.SeverityCritical, + findings: []detector.RawFinding{{ + DetectorID: "provider-specialized", + Raw: data[wholeStart:wholeEnd], + Redacted: "provider-redacted", + }}, + }} + + eng := New(Config{Concurrency: 1, Detectors: []detector.Detector{fallback, specialized}, Clock: fixedClock}) + result, err := eng.Scan(context.Background(), &mockSource{chunks: []source.Chunk{{ + Data: data, SourceMetadata: finding.SourceMetadata{FilePath: "appsettings.json"}, + }}}) + require.NoError(t, err) + require.Len(t, result.Findings, 1) + assert.Equal(t, "provider-specialized", result.Findings[0].DetectorID) + assert.Equal(t, finding.SeverityCritical, result.Findings[0].Severity) +} + +func TestScan_NonAuthorityOverlapCannotEraseFindingThroughSeverityFilter(t *testing.T) { + data := []byte(`{"Password":"fixture-secret-value"}`) + start := bytes.Index(data, []byte("fixture-secret-value")) + end := start + len("fixture-secret-value") + fallback := &fallbackMockDetector{mockDetector: &mockDetector{ + id: "structured-fallback", + severity: finding.SeverityHigh, + findings: []detector.RawFinding{{ + DetectorID: "structured-fallback", Raw: data[start:end], Redacted: "fallback", ByteStart: start, ByteEnd: end, + }}, + }} + customLike := &mockDetector{ + id: "custom-low", + severity: finding.SeverityLow, + findings: []detector.RawFinding{{ + DetectorID: "custom-low", Raw: data[start:end], Redacted: "custom", ByteStart: start, ByteEnd: end, + }}, + } + + eng := New(Config{ + Concurrency: 1, + Detectors: []detector.Detector{fallback, customLike}, + MinSeverity: finding.SeverityHigh, + Clock: fixedClock, + }) + result, err := eng.Scan(context.Background(), &mockSource{chunks: []source.Chunk{{Data: data}}}) + require.NoError(t, err) + require.Len(t, result.Findings, 1) + assert.Equal(t, "structured-fallback", result.Findings[0].DetectorID) +} + +func TestSuppressFallbacksForSpecialized_PreservesDistinctLocations(t *testing.T) { + makePair := func(id string, start, end int, severity finding.Severity) locatedVerifyPair { + return locatedVerifyPair{ + pair: verifier.VerifyPair{Finding: finding.Finding{DetectorID: id, Severity: severity}}, + start: start, + end: end, + } + } + tests := []struct { + name string + fallbacks []locatedVerifyPair + specialized locatedVerifyPair + want []string + }{ + { + name: "same value at distinct location remains", + fallbacks: []locatedVerifyPair{ + makePair("fallback", 10, 20, finding.SeverityHigh), + }, + specialized: func() locatedVerifyPair { + pair := makePair("specialized", 30, 40, finding.SeverityCritical) + pair.isAuthority = true + return pair + }(), + want: []string{"fallback"}, + }, + { + name: "specialized subset suppresses containing fallback", + fallbacks: []locatedVerifyPair{ + makePair("fallback", 10, 40, finding.SeverityHigh), + }, + specialized: func() locatedVerifyPair { + pair := makePair("specialized", 20, 30, finding.SeverityCritical) + pair.isAuthority = true + return pair + }(), + want: []string{}, + }, + { + name: "unknown fallback location is not suppressed", + fallbacks: []locatedVerifyPair{ + makePair("fallback", -1, -1, finding.SeverityHigh), + }, + specialized: func() locatedVerifyPair { + pair := makePair("specialized", 10, 20, finding.SeverityCritical) + pair.isAuthority = true + return pair + }(), + want: []string{"fallback"}, + }, + { + name: "overlapping non-authority cannot suppress", + fallbacks: []locatedVerifyPair{ + makePair("fallback", 10, 20, finding.SeverityHigh), + }, + specialized: makePair("custom", 10, 20, finding.SeverityLow), + want: []string{"fallback"}, + }, + { + name: "lower-severity authority cannot suppress", + fallbacks: []locatedVerifyPair{ + makePair("fallback", 10, 20, finding.SeverityHigh), + }, + specialized: func() locatedVerifyPair { + pair := makePair("specialized", 10, 20, finding.SeverityLow) + pair.isAuthority = true + return pair + }(), + want: []string{"fallback"}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := suppressFallbacksForSpecialized(tc.fallbacks, tc.specialized) + ids := make([]string, 0, len(got)) + for _, pair := range got { + ids = append(ids, pair.pair.Finding.DetectorID) + } + assert.Equal(t, tc.want, ids) + }) + } +} + func TestScan_EmptySource_ReturnsNoFindings(t *testing.T) { src := &mockSource{chunks: nil} det := &mockDetector{id: "det", findings: nil} @@ -336,6 +524,27 @@ func TestScan_RepeatedSecret_DistinctLinesAndIDs(t *testing.T) { assert.NotEqual(t, result.Findings[0].ID, result.Findings[1].ID, "distinct lines must yield distinct IDs") } +func TestRawFindingOffset_ExactSpanWinsAndInvalidSpanFallsBack(t *testing.T) { + rawBytes := []byte("SAME_SECRET") + data := []byte("SAME_SECRET decoy\nSAME_SECRET actual\n") + second := bytes.LastIndex(data, rawBytes) + + exact := detector.RawFinding{ + Raw: rawBytes, + ByteStart: second, + ByteEnd: second + len(rawBytes), + } + assert.Equal(t, second, rawFindingOffset(data, exact, nil)) + + invalid := exact + invalid.ByteEnd-- // span bytes no longer equal Raw + assert.Equal(t, 0, rawFindingOffset(data, invalid, nil), "invalid detector spans must use the legacy safe fallback") + + outOfBounds := exact + outOfBounds.ByteEnd = len(data) + 1 + assert.Equal(t, 0, rawFindingOffset(data, outOfBounds, nil)) +} + func TestScan_RepeatedSecret_FirstIgnored_SecondReported(t *testing.T) { // Line 1 carries an ignore marker; line 3 is a real leak. The real leak // MUST still be reported — regression test for the false-negative where an @@ -776,6 +985,84 @@ func TestScan_EntropyGating_StructuralDetectorNeverGated(t *testing.T) { assert.Len(t, result.Findings, 1, "structural detectors must never be entropy-gated") } +func TestScan_EntropyGating_PerFindingPolicy(t *testing.T) { + low := "01234567012345670123456701234567" + src := &mockSource{ + chunks: []source.Chunk{ + {Data: []byte(low), SourceMetadata: finding.SourceMetadata{FilePath: "f.txt"}}, + }, + } + base := &mockDetector{ + id: "mixed", + findings: []detector.RawFinding{{DetectorID: "mixed", Raw: []byte(low), Redacted: "r***"}}, + entropyBased: true, + } + + for _, tc := range []struct { + name string + gateFinding bool + wantCount int + }{ + {name: "strong contextual finding bypasses gate", gateFinding: false, wantCount: 1}, + {name: "heuristic finding retains gate", gateFinding: true, wantCount: 0}, + } { + t.Run(tc.name, func(t *testing.T) { + det := &mixedEntropyDetector{mockDetector: base, gateFinding: tc.gateFinding} + eng := New(Config{ + Concurrency: 1, + Detectors: []detector.Detector{det}, + EnableEntropy: true, + EntropyThreshold: 4.0, + Clock: fixedClock, + }) + result, err := eng.Scan(context.Background(), src) + require.NoError(t, err) + assert.Len(t, result.Findings, tc.wantCount) + }) + } +} + +func TestScan_EntropyGating_PerFindingPolicyCannotOptInStructuralDetector(t *testing.T) { + low := "01234567012345670123456701234567" + src := &mockSource{chunks: []source.Chunk{{ + Data: []byte(low), + SourceMetadata: finding.SourceMetadata{FilePath: "f.txt"}, + }}} + raw := detector.RawFinding{DetectorID: "structural", Raw: []byte(low), Redacted: "r***"} + + tests := []struct { + name string + det detector.Detector + }{ + { + name: "EntropyBased false", + det: &mixedEntropyDetector{ + mockDetector: &mockDetector{id: "mixed-structural", findings: []detector.RawFinding{raw}}, + gateFinding: true, + }, + }, + { + name: "EntropyBased absent", + det: &policyOnlyDetector{findings: []detector.RawFinding{raw}}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + eng := New(Config{ + Concurrency: 1, + Detectors: []detector.Detector{tc.det}, + EnableEntropy: true, + EntropyThreshold: 4.0, + Clock: fixedClock, + }) + result, err := eng.Scan(context.Background(), src) + require.NoError(t, err) + assert.Len(t, result.Findings, 1, "per-finding refinement must never opt a structural detector into gating") + }) + } +} + func TestScan_EntropyDisabled_NoGating(t *testing.T) { // Low-entropy value must be reported when entropy analysis is off. src := &mockSource{ diff --git a/internal/matcher/matcher.go b/internal/matcher/matcher.go index 65bd252..192ebe5 100644 --- a/internal/matcher/matcher.go +++ b/internal/matcher/matcher.go @@ -4,6 +4,7 @@ package matcher import ( + "sort" "strings" "sync" @@ -116,6 +117,7 @@ func (m *Matcher) Match(data []byte) []detector.Detector { for _, det := range m.allDetectors { result = append(result, det) } + sortDetectors(result) return result } @@ -152,5 +154,12 @@ func (m *Matcher) Match(data []byte) []detector.Detector { result = append(result, det) } } + sortDetectors(result) return result } + +func sortDetectors(detectors []detector.Detector) { + sort.Slice(detectors, func(i, j int) bool { + return detectors[i].ID() < detectors[j].ID() + }) +} diff --git a/internal/matcher/matcher_test.go b/internal/matcher/matcher_test.go index 1d30902..a71117c 100644 --- a/internal/matcher/matcher_test.go +++ b/internal/matcher/matcher_test.go @@ -74,6 +74,27 @@ func TestMatch_MultipleDetectors_ReturnsOnlyMatched(t *testing.T) { assert.Equal(t, "github", result[0].ID()) } +func TestMatch_ReturnsDetectorsInDeterministicIDOrder(t *testing.T) { + m := New([]detector.Detector{ + &stubDetector{id: "z-detector", keywords: []string{"shared"}}, + &stubDetector{id: "a-detector", keywords: []string{"shared"}}, + &stubDetector{id: "m-detector", keywords: nil}, + }) + + want := []string{"a-detector", "m-detector", "z-detector"} + for iteration := 0; iteration < 50; iteration++ { + assert.Equal(t, want, detectorIDs(m.Match([]byte("SHARED")))) + } +} + +func TestMatch_NoKeywordOnly_ReturnsDetectorsInDeterministicIDOrder(t *testing.T) { + m := New([]detector.Detector{ + &stubDetector{id: "z-detector"}, + &stubDetector{id: "a-detector"}, + }) + assert.Equal(t, []string{"a-detector", "z-detector"}, detectorIDs(m.Match([]byte("anything")))) +} + func TestMatch_MultipleKeywordsHit_ReturnsUniqueDetectors(t *testing.T) { m := New([]detector.Detector{ &stubDetector{id: "aws", keywords: []string{"AKIA", "ASIA"}}, diff --git a/internal/meta/capabilities.go b/internal/meta/capabilities.go new file mode 100644 index 0000000..c5ae1ec --- /dev/null +++ b/internal/meta/capabilities.go @@ -0,0 +1,189 @@ +package meta + +// VerifierKind describes what a detector can truthfully prove in the normal +// production pipeline. It is deliberately independent of registry presence: +// a registered verifier may still need context the detector cannot supply, or +// may perform only offline format validation. +type VerifierKind string + +const ( + VerifierLive VerifierKind = "live" + VerifierFormatOnly VerifierKind = "format_only" + VerifierRequiresContext VerifierKind = "requires_context" + VerifierNone VerifierKind = "none" +) + +// EndpointClass describes how a verifier selects the endpoint that receives a +// credential. Values are closed and validated in tests so a typo cannot +// silently create a new routing contract. +type EndpointClass string + +const ( + EndpointNone EndpointClass = "none" + EndpointOfflineFormat EndpointClass = "offline_format" + EndpointFixedProviderAPI EndpointClass = "fixed_provider_api" + EndpointFixedProviderSDK EndpointClass = "fixed_provider_sdk" + EndpointRegionalProviderAPI EndpointClass = "regional_provider_api" + EndpointBoundedRegionalProviderAPI EndpointClass = "bounded_regional_provider_api" + EndpointIssuerDerivedProviderAPI EndpointClass = "issuer_derived_provider_api" + EndpointDetectorContextProviderAPI EndpointClass = "detector_context_provider_api" + EndpointOperatorContextProviderAPI EndpointClass = "operator_context_provider_api" + EndpointCompanionContextProviderAPI EndpointClass = "companion_context_provider_api" + EndpointCompanionContextFixedAPI EndpointClass = "companion_context_fixed_provider_api" + EndpointEmbeddedProviderURL EndpointClass = "credential_embedded_provider_url" +) + +// InactiveStatusContract identifies the exact class of evidence that may +// produce verified_inactive. It is deliberately distinct from EndpointClass. +type InactiveStatusContract string + +const ( + InactiveNone InactiveStatusContract = "none" + InactiveDefinitiveAuthRejection InactiveStatusContract = "definitive_provider_auth_rejection" + InactiveHTTP401Only InactiveStatusContract = "http_401_only" + InactiveAllRegionsHTTP401 InactiveStatusContract = "all_regions_http_401" + InactiveRegionAppropriateRejection InactiveStatusContract = "region_appropriate_auth_rejection" + InactiveProviderBodyRejection InactiveStatusContract = "provider_body_auth_rejection" + InactiveProviderSpecificRejection InactiveStatusContract = "provider_specific_definitive_rejection" + InactivePairedAuthRejection InactiveStatusContract = "paired_credential_auth_rejection" + InactiveTrustedInstanceHTTP401 InactiveStatusContract = "trusted_instance_http_401" + InactiveTrustedIssuerHTTP401 InactiveStatusContract = "trusted_issuer_http_401" + InactiveTrustedOriginHTTP401 InactiveStatusContract = "trusted_origin_http_401" + InactiveTrustedOriginInvalid401 InactiveStatusContract = "trusted_origin_standard_invalid_token_401" + InactiveTrustedOriginRejection InactiveStatusContract = "trusted_origin_auth_rejection" + InactiveTrustedSiteRejection InactiveStatusContract = "trusted_site_auth_rejection" + InactiveTrustedStoreHTTP401 InactiveStatusContract = "trusted_store_http_401" + InactiveTrustedStoreRejection InactiveStatusContract = "trusted_store_auth_rejection" + InactiveTypedAuthenticationError InactiveStatusContract = "typed_authentication_error" +) + +// VerificationCapability is the canonical machine-readable contract for one +// built-in detector. Empty ProviderRegions means the provider exposes one +// global endpoint class or the region is encoded by trusted credential context. +// LastContractReviewedAt and ContractReferenceURLs are intentionally empty +// until the provider contract has been checked against current primary +// documentation; an empty value must never be presented as a completed audit. +type VerificationCapability struct { + DetectorID string + VerifierKind VerifierKind + RequiredContextFields []string + ProviderRegions []string + VerifiableSubtypes []string + UnverifiableSubtypes []string + EndpointClass EndpointClass + InactiveStatusContract InactiveStatusContract + LastContractReviewedAt string + ContractReferenceURLs []string +} + +// verificationCapabilities is sorted by detector ID. Keep entries compact and +// factual: provider-specific prose belongs in user documentation, while this +// manifest owns categorization, routing class, and inactive-proof semantics. +var verificationCapabilities = []VerificationCapability{ + {DetectorID: "airtable-pat", VerifierKind: VerifierLive, EndpointClass: "fixed_provider_api", InactiveStatusContract: "definitive_provider_auth_rejection"}, + {DetectorID: "anthropic-api-key", VerifierKind: VerifierLive, EndpointClass: "fixed_provider_api", InactiveStatusContract: "definitive_provider_auth_rejection"}, + {DetectorID: "auth0-management-token", VerifierKind: VerifierRequiresContext, RequiredContextFields: []string{"trusted_api_origin"}, ProviderRegions: []string{"Auth0 tenant", "Custom domain"}, EndpointClass: "operator_context_provider_api", InactiveStatusContract: "trusted_origin_http_401", LastContractReviewedAt: "2026-08-11", ContractReferenceURLs: []string{"https://auth0.com/docs/secure/tokens/access-tokens/validate-access-tokens"}}, + {DetectorID: "aws-access-key-id", VerifierKind: VerifierLive, RequiredContextFields: []string{"raw_v2"}, EndpointClass: "fixed_provider_sdk", InactiveStatusContract: "typed_authentication_error"}, + {DetectorID: "azure-entra-secret", VerifierKind: VerifierFormatOnly, EndpointClass: "offline_format", InactiveStatusContract: "none"}, + {DetectorID: "azure-storage-key", VerifierKind: VerifierFormatOnly, EndpointClass: "offline_format", InactiveStatusContract: "none"}, + {DetectorID: "bitbucket-app-password", VerifierKind: VerifierLive, RequiredContextFields: []string{"username"}, EndpointClass: "fixed_provider_api", InactiveStatusContract: "definitive_provider_auth_rejection"}, + {DetectorID: "circleci-token", VerifierKind: VerifierLive, EndpointClass: "fixed_provider_api", InactiveStatusContract: "definitive_provider_auth_rejection"}, + {DetectorID: "cloudflare-api-token", VerifierKind: VerifierLive, EndpointClass: "fixed_provider_api", InactiveStatusContract: "definitive_provider_auth_rejection"}, + {DetectorID: "coinbase-api-key", VerifierKind: VerifierFormatOnly, EndpointClass: "offline_format", InactiveStatusContract: "none"}, + {DetectorID: "database-connection-string", VerifierKind: VerifierNone, EndpointClass: "none", InactiveStatusContract: "none"}, + {DetectorID: "databricks-token", VerifierKind: VerifierLive, RequiredContextFields: []string{"host"}, EndpointClass: "detector_context_provider_api", InactiveStatusContract: "definitive_provider_auth_rejection"}, + {DetectorID: "datadog-api-key", VerifierKind: VerifierRequiresContext, RequiredContextFields: []string{"trusted_api_origin"}, ProviderRegions: []string{"US1", "US3", "US5", "EU", "AP1", "AP2", "UK1", "US1-FED", "US2-FED"}, EndpointClass: "operator_context_provider_api", InactiveStatusContract: "trusted_site_auth_rejection", LastContractReviewedAt: "2026-08-11", ContractReferenceURLs: []string{"https://docs.datadoghq.com/api/latest/authentication/", "https://docs.datadoghq.com/getting_started/application/"}}, + {DetectorID: "deepseek-api-key", VerifierKind: VerifierLive, EndpointClass: "fixed_provider_api", InactiveStatusContract: "definitive_provider_auth_rejection"}, + {DetectorID: "digitalocean-token", VerifierKind: VerifierLive, EndpointClass: "fixed_provider_api", InactiveStatusContract: "definitive_provider_auth_rejection"}, + {DetectorID: "discord-bot-token", VerifierKind: VerifierLive, EndpointClass: "fixed_provider_api", InactiveStatusContract: "definitive_provider_auth_rejection"}, + {DetectorID: "discord-webhook-url", VerifierKind: VerifierNone, EndpointClass: "none", InactiveStatusContract: "none"}, + {DetectorID: "dockerhub-pat", VerifierKind: VerifierLive, EndpointClass: "fixed_provider_api", InactiveStatusContract: "definitive_provider_auth_rejection"}, + {DetectorID: "doppler-token", VerifierKind: VerifierLive, EndpointClass: "fixed_provider_api", InactiveStatusContract: "definitive_provider_auth_rejection"}, + {DetectorID: "figma-pat", VerifierKind: VerifierLive, EndpointClass: "fixed_provider_api", InactiveStatusContract: "definitive_provider_auth_rejection"}, + {DetectorID: "ftp-credentials", VerifierKind: VerifierNone, EndpointClass: "none", InactiveStatusContract: "none"}, + {DetectorID: "gcp-service-account", VerifierKind: VerifierFormatOnly, EndpointClass: "offline_format", InactiveStatusContract: "none"}, + {DetectorID: "generic-api-key", VerifierKind: VerifierNone, EndpointClass: "none", InactiveStatusContract: "none"}, + {DetectorID: "github-oauth-token", VerifierKind: VerifierRequiresContext, RequiredContextFields: []string{"trusted_api_origin"}, ProviderRegions: []string{"GitHub.com", "GHES"}, VerifiableSubtypes: []string{"gho", "ghu", "ghs"}, UnverifiableSubtypes: []string{"ghr"}, EndpointClass: "operator_context_provider_api", InactiveStatusContract: "trusted_issuer_http_401", LastContractReviewedAt: "2026-08-11", ContractReferenceURLs: []string{"https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps", "https://docs.github.com/en/rest/apps/installations#list-repositories-accessible-to-the-app-installation", "https://docs.github.com/en/rest/users/users#get-the-authenticated-user", "https://github.blog/changelog/2021-07-26-expiration-options-for-personal-access-tokens/"}}, + {DetectorID: "github-token", VerifierKind: VerifierRequiresContext, RequiredContextFields: []string{"trusted_api_origin"}, ProviderRegions: []string{"GitHub.com", "GHES"}, EndpointClass: "operator_context_provider_api", InactiveStatusContract: "trusted_issuer_http_401", LastContractReviewedAt: "2026-08-11", ContractReferenceURLs: []string{"https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps", "https://docs.github.com/en/rest/users/users#get-the-authenticated-user", "https://github.blog/changelog/2021-07-26-expiration-options-for-personal-access-tokens/"}}, + {DetectorID: "gitlab-pat", VerifierKind: VerifierRequiresContext, RequiredContextFields: []string{"trusted_api_origin"}, ProviderRegions: []string{"GitLab.com", "Self-managed"}, VerifiableSubtypes: []string{"glpat"}, UnverifiableSubtypes: []string{"gldt", "glrt", "glrtr", "glcbt", "glptt", "glimt", "glagent", "glwt", "glsoat", "glffct", "gloas", "glft"}, EndpointClass: "operator_context_provider_api", InactiveStatusContract: "trusted_origin_standard_invalid_token_401", LastContractReviewedAt: "2026-08-11", ContractReferenceURLs: []string{"https://docs.gitlab.com/api/users/#retrieve-the-current-user", "https://docs.gitlab.com/api/personal_access_tokens/"}}, + {DetectorID: "grafana-api-key", VerifierKind: VerifierRequiresContext, RequiredContextFields: []string{"trusted_instance_origin"}, EndpointClass: "operator_context_provider_api", InactiveStatusContract: "trusted_instance_http_401", LastContractReviewedAt: "2026-08-10", ContractReferenceURLs: []string{"https://grafana.com/docs/grafana/latest/developers/http_api/user/#actual-user"}}, + {DetectorID: "hashicorp-vault-token", VerifierKind: VerifierNone, EndpointClass: "none", InactiveStatusContract: "none"}, + {DetectorID: "heroku-api-key", VerifierKind: VerifierLive, EndpointClass: "fixed_provider_api", InactiveStatusContract: "definitive_provider_auth_rejection"}, + {DetectorID: "huggingface-token", VerifierKind: VerifierLive, EndpointClass: "fixed_provider_api", InactiveStatusContract: "definitive_provider_auth_rejection"}, + {DetectorID: "infura-api-key", VerifierKind: VerifierLive, EndpointClass: "fixed_provider_api", InactiveStatusContract: "definitive_provider_auth_rejection"}, + {DetectorID: "jwt", VerifierKind: VerifierNone, EndpointClass: "none", InactiveStatusContract: "none"}, + {DetectorID: "launchdarkly-sdk-key", VerifierKind: VerifierLive, EndpointClass: "fixed_provider_api", InactiveStatusContract: "definitive_provider_auth_rejection"}, + {DetectorID: "ldap-credentials", VerifierKind: VerifierNone, EndpointClass: "none", InactiveStatusContract: "none"}, + {DetectorID: "linear-api-key", VerifierKind: VerifierLive, EndpointClass: "fixed_provider_api", InactiveStatusContract: "definitive_provider_auth_rejection"}, + {DetectorID: "mailgun-api-key", VerifierKind: VerifierLive, ProviderRegions: []string{"US", "EU"}, EndpointClass: "regional_provider_api", InactiveStatusContract: "region_appropriate_auth_rejection"}, + {DetectorID: "newrelic-api-key", VerifierKind: VerifierLive, ProviderRegions: []string{"US", "EU"}, EndpointClass: "bounded_regional_provider_api", InactiveStatusContract: "all_regions_http_401", LastContractReviewedAt: "2026-08-10", ContractReferenceURLs: []string{"https://docs.newrelic.com/docs/apis/nerdgraph/get-started/introduction-new-relic-nerdgraph/"}}, + {DetectorID: "notion-token", VerifierKind: VerifierLive, EndpointClass: "fixed_provider_api", InactiveStatusContract: "definitive_provider_auth_rejection"}, + {DetectorID: "npm-token", VerifierKind: VerifierLive, EndpointClass: "fixed_provider_api", InactiveStatusContract: "definitive_provider_auth_rejection"}, + {DetectorID: "okta-api-token", VerifierKind: VerifierLive, RequiredContextFields: []string{"domain"}, EndpointClass: "detector_context_provider_api", InactiveStatusContract: "definitive_provider_auth_rejection"}, + {DetectorID: "openai-api-key", VerifierKind: VerifierLive, EndpointClass: "fixed_provider_api", InactiveStatusContract: "definitive_provider_auth_rejection"}, + {DetectorID: "pagerduty-api-key", VerifierKind: VerifierLive, EndpointClass: "fixed_provider_api", InactiveStatusContract: "definitive_provider_auth_rejection"}, + {DetectorID: "postmark-server-token", VerifierKind: VerifierLive, EndpointClass: "fixed_provider_api", InactiveStatusContract: "definitive_provider_auth_rejection"}, + {DetectorID: "private-key", VerifierKind: VerifierNone, EndpointClass: "none", InactiveStatusContract: "none"}, + {DetectorID: "pypi-api-token", VerifierKind: VerifierLive, EndpointClass: "fixed_provider_api", InactiveStatusContract: "definitive_provider_auth_rejection"}, + {DetectorID: "rabbitmq-connection-string", VerifierKind: VerifierFormatOnly, EndpointClass: "offline_format", InactiveStatusContract: "none"}, + {DetectorID: "redis-connection-string", VerifierKind: VerifierNone, EndpointClass: "none", InactiveStatusContract: "none"}, + {DetectorID: "rubygems-api-key", VerifierKind: VerifierLive, EndpointClass: "fixed_provider_api", InactiveStatusContract: "definitive_provider_auth_rejection"}, + {DetectorID: "sendgrid-api-key", VerifierKind: VerifierLive, EndpointClass: "fixed_provider_api", InactiveStatusContract: "http_401_only"}, + {DetectorID: "sentry-token", VerifierKind: VerifierLive, EndpointClass: "fixed_provider_api", InactiveStatusContract: "definitive_provider_auth_rejection"}, + {DetectorID: "shopify-access-token", VerifierKind: VerifierRequiresContext, RequiredContextFields: []string{"trusted_store_origin"}, EndpointClass: "operator_context_provider_api", InactiveStatusContract: "trusted_store_http_401", LastContractReviewedAt: "2026-08-11", ContractReferenceURLs: []string{"https://shopify.dev/docs/api/admin-graphql/latest/queries/shop"}}, + {DetectorID: "slack-token", VerifierKind: VerifierLive, EndpointClass: "fixed_provider_api", InactiveStatusContract: "provider_body_auth_rejection", LastContractReviewedAt: "2026-08-11", ContractReferenceURLs: []string{"https://api.slack.com/methods/auth.test"}}, + {DetectorID: "slack-webhook", VerifierKind: VerifierNone, EndpointClass: "none", InactiveStatusContract: "none"}, + {DetectorID: "snowflake-credentials", VerifierKind: VerifierFormatOnly, EndpointClass: "offline_format", InactiveStatusContract: "none"}, + {DetectorID: "snyk-api-key", VerifierKind: VerifierRequiresContext, RequiredContextFields: []string{"trusted_api_origin"}, ProviderRegions: []string{"US1", "US2", "EU", "AU", "GOV", "PRIVATE"}, EndpointClass: "operator_context_provider_api", InactiveStatusContract: "trusted_origin_http_401", LastContractReviewedAt: "2026-08-11", ContractReferenceURLs: []string{"https://docs.snyk.io/snyk-api/reference/users", "https://docs.snyk.io/snyk-api/authentication-for-api"}}, + {DetectorID: "sonarcloud-token", VerifierKind: VerifierLive, EndpointClass: "fixed_provider_api", InactiveStatusContract: "definitive_provider_auth_rejection"}, + {DetectorID: "stripe-api-key-live", VerifierKind: VerifierLive, EndpointClass: "fixed_provider_api", InactiveStatusContract: "definitive_provider_auth_rejection"}, + {DetectorID: "stripe-api-key-test", VerifierKind: VerifierLive, EndpointClass: "fixed_provider_api", InactiveStatusContract: "definitive_provider_auth_rejection"}, + {DetectorID: "structured-config-secret", VerifierKind: VerifierNone, EndpointClass: "none", InactiveStatusContract: "none"}, + {DetectorID: "supabase-service-key", VerifierKind: VerifierLive, EndpointClass: "fixed_provider_api", InactiveStatusContract: "http_401_only", LastContractReviewedAt: "2026-08-11", ContractReferenceURLs: []string{"https://supabase.com/docs/reference/api/introduction"}}, + {DetectorID: "teams-webhook", VerifierKind: VerifierLive, EndpointClass: "credential_embedded_provider_url", InactiveStatusContract: "provider_specific_definitive_rejection"}, + {DetectorID: "telegram-bot-token", VerifierKind: VerifierLive, EndpointClass: "credential_embedded_provider_url", InactiveStatusContract: "definitive_provider_auth_rejection"}, + {DetectorID: "terraform-cloud-token", VerifierKind: VerifierLive, EndpointClass: "fixed_provider_api", InactiveStatusContract: "definitive_provider_auth_rejection"}, + {DetectorID: "twilio-api-key", VerifierKind: VerifierRequiresContext, RequiredContextFields: []string{"api_key_sid", "trusted_api_origin"}, ProviderRegions: []string{"US1", "IE1", "AU1"}, EndpointClass: "operator_context_provider_api", InactiveStatusContract: "trusted_origin_http_401", LastContractReviewedAt: "2026-08-11", ContractReferenceURLs: []string{"https://www.twilio.com/docs/iam/api/keys"}}, + {DetectorID: "vercel-token", VerifierKind: VerifierLive, EndpointClass: "fixed_provider_api", InactiveStatusContract: "definitive_provider_auth_rejection"}, +} + +// VerificationCapabilities returns a deep copy so callers cannot mutate the +// canonical manifest or any of its context/region slices. +func VerificationCapabilities() []VerificationCapability { + out := make([]VerificationCapability, len(verificationCapabilities)) + for i, capability := range verificationCapabilities { + out[i] = capability + out[i].RequiredContextFields = append([]string(nil), capability.RequiredContextFields...) + out[i].ProviderRegions = append([]string(nil), capability.ProviderRegions...) + out[i].VerifiableSubtypes = append([]string(nil), capability.VerifiableSubtypes...) + out[i].UnverifiableSubtypes = append([]string(nil), capability.UnverifiableSubtypes...) + out[i].ContractReferenceURLs = append([]string(nil), capability.ContractReferenceURLs...) + } + return out +} + +// CapabilityCounts summarizes the canonical manifest by verifier kind. +type CapabilityCounts struct { + Live int + FormatOnly int + RequiresContext int + None int +} + +// VerificationCapabilityCounts returns the current category totals. +func VerificationCapabilityCounts() CapabilityCounts { + var counts CapabilityCounts + for _, capability := range verificationCapabilities { + switch capability.VerifierKind { + case VerifierLive: + counts.Live++ + case VerifierFormatOnly: + counts.FormatOnly++ + case VerifierRequiresContext: + counts.RequiresContext++ + case VerifierNone: + counts.None++ + } + } + return counts +} diff --git a/internal/meta/capabilities_docs_test.go b/internal/meta/capabilities_docs_test.go new file mode 100644 index 0000000..0427ba0 --- /dev/null +++ b/internal/meta/capabilities_docs_test.go @@ -0,0 +1,225 @@ +package meta + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + markdownDetectorRow = regexp.MustCompile("^\\| `([^`]+)` \\|") + siteCapabilityRow = regexp.MustCompile(`(?s)
.*?data-i18n="verify\.(live|context|fmt|no)\.k".*?(\d+).*?
`) + siteStatCell = regexp.MustCompile(`(?s)
(\d+)
`) +) + +type documentedCategory struct { + heading string + kind VerifierKind +} + +func TestVerificationCoverageDocs_MatchCapabilityManifest(t *testing.T) { + counts := VerificationCapabilityCounts() + tests := []struct { + name string + path string + categories []documentedCategory + summary []string + }{ + { + name: "English", + path: filepath.Join("..", "..", "docs", "user-manuals", "en", "verification", "verification-coverage.md"), + categories: []documentedCategory{ + {heading: "## Live-verified", kind: VerifierLive}, + {heading: "## Requires trusted or companion context", kind: VerifierRequiresContext}, + {heading: "## Format-validated only", kind: VerifierFormatOnly}, + {heading: "## Not verifiable", kind: VerifierNone}, + }, + summary: []string{ + fmt.Sprintf("| Live-verified | %d |", counts.Live), + fmt.Sprintf("| Requires trusted/companion context | %d |", counts.RequiresContext), + fmt.Sprintf("| Format-validated only | %d |", counts.FormatOnly), + fmt.Sprintf("| Not verifiable | %d |", counts.None), + fmt.Sprintf("| **Total detectors** | **%d** |", Detectors), + }, + }, + { + name: "Turkish", + path: filepath.Join("..", "..", "docs", "user-manuals", "tr", "verification", "verification-coverage.md"), + categories: []documentedCategory{ + {heading: "## Canlı doğrulanan", kind: VerifierLive}, + {heading: "## Güvenilir veya eşlik eden bağlam gerektiren", kind: VerifierRequiresContext}, + {heading: "## Yalnızca format doğrulaması", kind: VerifierFormatOnly}, + {heading: "## Doğrulanamaz", kind: VerifierNone}, + }, + summary: []string{ + fmt.Sprintf("| Canlı doğrulanan | %d |", counts.Live), + fmt.Sprintf("| Güvenilir/eşlik eden bağlam gerektiren | %d |", counts.RequiresContext), + fmt.Sprintf("| Yalnızca format doğrulaması | %d |", counts.FormatOnly), + fmt.Sprintf("| Doğrulanamaz | %d |", counts.None), + fmt.Sprintf("| **Toplam dedektör** | **%d** |", Detectors), + }, + }, + } + + want := make(map[string]VerifierKind, Detectors) + for _, capability := range VerificationCapabilities() { + want[capability.DetectorID] = capability.VerifierKind + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + contents, err := os.ReadFile(tc.path) + require.NoError(t, err) + got := parseDocumentedCapabilities(t, string(contents), tc.categories) + assert.Equal(t, want, got, "documentation categories drifted from capability manifest") + for _, summary := range tc.summary { + assert.Contains(t, string(contents), summary) + } + }) + } +} + +func TestPublishedCapabilityClaims_MatchManifest(t *testing.T) { + counts := VerificationCapabilityCounts() + require.Equal(t, CapabilityCounts{Live: 39, FormatOnly: 6, RequiresContext: 9, None: 11}, counts) + registryCoverage := 100 * float64(Verifiers) / float64(Detectors) + + tests := []struct { + path string + required []string + forbid []string + }{ + { + path: filepath.Join("..", "..", "README.md"), + required: []string{ + fmt.Sprintf("**%d built-in detectors**", Detectors), + fmt.Sprintf("**%d direct live checks** + **%d context-required checks** + **%d offline format validators**", counts.Live, counts.RequiresContext, counts.FormatOnly), + fmt.Sprintf("**%d of %d detectors (%.1f%%)**", Verifiers, Detectors, registryCoverage), + }, + forbid: []string{"54 live verifiers", "54 verifiers, 84.4%", "~48 detectors"}, + }, + { + path: filepath.Join("..", "..", "site", "index.html"), + required: []string{ + fmt.Sprintf("%d detectors · %d direct-live · %d context-required · %d sources", Detectors, counts.Live, counts.RequiresContext, Sources), + fmt.Sprintf(`%d`, counts.Live), + fmt.Sprintf(`%d`, counts.RequiresContext), + fmt.Sprintf(`%d`, counts.FormatOnly), + fmt.Sprintf(`%d`, counts.None), + }, + forbid: []string{"54 live verifiers", "~49", "Redacted index — 64 detectors"}, + }, + { + path: filepath.Join("..", "..", "site", "js", "translations.js"), + required: []string{ + fmt.Sprintf(`"hero.kv": "%d detectors · %d direct-live · %d context-required · %d sources"`, Detectors, counts.Live, counts.RequiresContext, Sources), + fmt.Sprintf(`"hero.kv": "%d dedektör · %d doğrudan canlı · %d bağlam gerektiren · %d kaynak"`, Detectors, counts.Live, counts.RequiresContext, Sources), + }, + forbid: []string{"54 live verifiers", "54 canlı doğrulayıcı", "64 detectors", "63 dedektör"}, + }, + { + path: filepath.Join("..", "..", "docs", "guides", "secret-verification.md"), + required: []string{ + fmt.Sprintf("**%d** detectors support a direct live check", counts.Live), + fmt.Sprintf("**%d** require trusted issuer, region, or companion context", counts.RequiresContext), + fmt.Sprintf("### Not Verifiable (%d detectors)", counts.None), + }, + forbid: []string{"Live API Verification (48 detectors)", "84.4% of its 64 detectors"}, + }, + } + + for _, tc := range tests { + t.Run(tc.path, func(t *testing.T) { + contents, err := os.ReadFile(tc.path) + require.NoError(t, err) + text := string(contents) + for _, required := range tc.required { + assert.Contains(t, text, required) + } + for _, forbidden := range tc.forbid { + assert.NotContains(t, text, forbidden) + } + }) + } +} + +func TestSiteVisibleCounts_MatchCapabilityManifest(t *testing.T) { + contents, err := os.ReadFile(filepath.Join("..", "..", "site", "index.html")) + require.NoError(t, err) + counts := VerificationCapabilityCounts() + + wantCapabilities := map[string]int{ + "live": counts.Live, "context": counts.RequiresContext, + "fmt": counts.FormatOnly, "no": counts.None, + } + gotCapabilities := make(map[string]int, len(wantCapabilities)) + for _, match := range siteCapabilityRow.FindAllStringSubmatch(string(contents), -1) { + var count int + _, err := fmt.Sscanf(match[2], "%d", &count) + require.NoError(t, err) + gotCapabilities[match[1]] = count + } + assert.Equal(t, wantCapabilities, gotCapabilities) + + wantStats := map[string]int{ + "detectors": Detectors, "verifiers": counts.Live, + "sources": Sources, "formats": OutputFormats, + } + gotStats := make(map[string]int, len(wantStats)) + for _, match := range siteStatCell.FindAllStringSubmatch(string(contents), -1) { + var count int + _, err := fmt.Sscanf(match[1], "%d", &count) + require.NoError(t, err) + gotStats[match[2]] = count + } + assert.Equal(t, wantStats, gotStats) +} + +func parseDocumentedCapabilities( + t *testing.T, + contents string, + categories []documentedCategory, +) map[string]VerifierKind { + t.Helper() + headingKinds := make(map[string]VerifierKind, len(categories)) + for _, category := range categories { + headingKinds[category.heading] = category.kind + } + + got := make(map[string]VerifierKind, Detectors) + current := VerifierKind("") + scanner := bufio.NewScanner(strings.NewReader(contents)) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "## ") { + current = "" + for heading, kind := range headingKinds { + if strings.HasPrefix(line, heading) { + current = kind + break + } + } + continue + } + if current == "" { + continue + } + match := markdownDetectorRow.FindStringSubmatch(line) + if match == nil { + continue + } + if previous, duplicate := got[match[1]]; duplicate { + t.Errorf("detector %q documented twice (%s and %s)", match[1], previous, current) + } + got[match[1]] = current + } + require.NoError(t, scanner.Err()) + return got +} diff --git a/internal/meta/capabilities_test.go b/internal/meta/capabilities_test.go new file mode 100644 index 0000000..13ba209 --- /dev/null +++ b/internal/meta/capabilities_test.go @@ -0,0 +1,257 @@ +package meta + +import ( + "net/url" + "regexp" + "sort" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var detectorIDPattern = regexp.MustCompile(`^[a-z0-9]+(?:-[a-z0-9]+)*$`) + +var validEndpointClasses = map[EndpointClass]struct{}{ + EndpointNone: {}, EndpointOfflineFormat: {}, EndpointFixedProviderAPI: {}, + EndpointFixedProviderSDK: {}, EndpointRegionalProviderAPI: {}, + EndpointBoundedRegionalProviderAPI: {}, EndpointIssuerDerivedProviderAPI: {}, + EndpointDetectorContextProviderAPI: {}, + EndpointOperatorContextProviderAPI: {}, EndpointCompanionContextProviderAPI: {}, + EndpointCompanionContextFixedAPI: {}, EndpointEmbeddedProviderURL: {}, +} + +var validInactiveContracts = map[InactiveStatusContract]struct{}{ + InactiveNone: {}, InactiveDefinitiveAuthRejection: {}, InactiveHTTP401Only: {}, + InactiveAllRegionsHTTP401: {}, InactiveRegionAppropriateRejection: {}, + InactiveProviderBodyRejection: {}, InactiveProviderSpecificRejection: {}, + InactivePairedAuthRejection: {}, InactiveTrustedInstanceHTTP401: {}, + InactiveTrustedIssuerHTTP401: {}, InactiveTrustedOriginHTTP401: {}, + InactiveTrustedOriginInvalid401: {}, + InactiveTrustedOriginRejection: {}, + InactiveTrustedSiteRejection: {}, InactiveTrustedStoreHTTP401: {}, + InactiveTrustedStoreRejection: {}, + InactiveTypedAuthenticationError: {}, +} + +func TestVerificationCapabilities_AreCompleteAndWellFormed(t *testing.T) { + capabilities := VerificationCapabilities() + require.Len(t, capabilities, Detectors) + + ids := make([]string, 0, len(capabilities)) + seen := make(map[string]struct{}, len(capabilities)) + for _, capability := range capabilities { + assert.Regexp(t, detectorIDPattern, capability.DetectorID) + if _, duplicate := seen[capability.DetectorID]; duplicate { + t.Errorf("duplicate capability for detector %q", capability.DetectorID) + } + seen[capability.DetectorID] = struct{}{} + ids = append(ids, capability.DetectorID) + assert.Contains(t, validEndpointClasses, capability.EndpointClass, capability.DetectorID) + assert.Contains(t, validInactiveContracts, capability.InactiveStatusContract, capability.DetectorID) + + switch capability.VerifierKind { + case VerifierLive: + assert.NotEqual(t, EndpointNone, capability.EndpointClass, capability.DetectorID) + assert.NotEqual(t, InactiveNone, capability.InactiveStatusContract, capability.DetectorID) + case VerifierFormatOnly: + assert.Empty(t, capability.RequiredContextFields, capability.DetectorID) + assert.Equal(t, EndpointOfflineFormat, capability.EndpointClass, capability.DetectorID) + assert.Equal(t, InactiveNone, capability.InactiveStatusContract, capability.DetectorID) + case VerifierRequiresContext: + assert.NotEmpty(t, capability.RequiredContextFields, capability.DetectorID) + assert.NotEqual(t, EndpointNone, capability.EndpointClass, capability.DetectorID) + assert.NotEqual(t, InactiveNone, capability.InactiveStatusContract, capability.DetectorID) + case VerifierNone: + assert.Empty(t, capability.RequiredContextFields, capability.DetectorID) + assert.Equal(t, EndpointNone, capability.EndpointClass, capability.DetectorID) + assert.Equal(t, InactiveNone, capability.InactiveStatusContract, capability.DetectorID) + default: + t.Errorf("detector %q has unknown verifier kind %q", capability.DetectorID, capability.VerifierKind) + } + + if capability.LastContractReviewedAt != "" { + _, err := time.Parse(time.DateOnly, capability.LastContractReviewedAt) + assert.NoError(t, err, capability.DetectorID) + assert.NotEmpty(t, capability.ContractReferenceURLs, capability.DetectorID) + } else { + assert.Empty(t, capability.ContractReferenceURLs, capability.DetectorID) + } + for _, reference := range capability.ContractReferenceURLs { + parsed, err := url.Parse(reference) + if assert.NoError(t, err, capability.DetectorID) { + assert.Equal(t, "https", parsed.Scheme, capability.DetectorID) + assert.NotEmpty(t, parsed.Hostname(), capability.DetectorID) + } + } + } + + sorted := append([]string(nil), ids...) + sort.Strings(sorted) + assert.Equal(t, sorted, ids, "capability manifest must remain sorted by detector ID") +} + +func TestVerificationCapabilities_ReviewedContractsAreFresh(t *testing.T) { + const maximumAuditAge = 180 * 24 * time.Hour + now := time.Now().UTC() + + for _, capability := range VerificationCapabilities() { + if capability.LastContractReviewedAt == "" { + continue + } + reviewedAt, err := time.Parse(time.DateOnly, capability.LastContractReviewedAt) + require.NoError(t, err, capability.DetectorID) + assert.False(t, reviewedAt.After(now), "%s contract review is future-dated", capability.DetectorID) + assert.LessOrEqual(t, now.Sub(reviewedAt), maximumAuditAge, + "%s provider contract audit is stale; review the primary references and update code/tests before advancing the date", + capability.DetectorID) + } +} + +func TestVerificationCapabilityCounts_MatchPublishedCategories(t *testing.T) { + counts := VerificationCapabilityCounts() + assert.Equal(t, CapabilityCounts{Live: 39, FormatOnly: 6, RequiresContext: 9, None: 11}, counts) + assert.Equal(t, Detectors, counts.Live+counts.FormatOnly+counts.RequiresContext+counts.None) + assert.Equal(t, Verifiers, counts.Live+counts.FormatOnly+counts.RequiresContext, + "every non-none capability has one registered verifier") +} + +func TestVerificationCapabilities_ReturnsDefensiveCopy(t *testing.T) { + first := VerificationCapabilities() + require.NotEmpty(t, first) + originalID := first[0].DetectorID + contextIndex, regionIndex, subtypeIndex, referenceIndex := -1, -1, -1, -1 + for i := range first { + if contextIndex < 0 && len(first[i].RequiredContextFields) > 0 { + contextIndex = i + } + if regionIndex < 0 && len(first[i].ProviderRegions) > 0 { + regionIndex = i + } + if subtypeIndex < 0 && len(first[i].VerifiableSubtypes) > 0 && len(first[i].UnverifiableSubtypes) > 0 { + subtypeIndex = i + } + if referenceIndex < 0 && len(first[i].ContractReferenceURLs) > 0 { + referenceIndex = i + } + } + require.NotEqual(t, -1, contextIndex) + require.NotEqual(t, -1, regionIndex) + require.NotEqual(t, -1, subtypeIndex) + require.NotEqual(t, -1, referenceIndex) + originalContext := first[contextIndex].RequiredContextFields[0] + originalRegion := first[regionIndex].ProviderRegions[0] + originalVerifiableSubtype := first[subtypeIndex].VerifiableSubtypes[0] + originalUnverifiableSubtype := first[subtypeIndex].UnverifiableSubtypes[0] + originalReference := first[referenceIndex].ContractReferenceURLs[0] + first[0].DetectorID = "mutated" + first[contextIndex].RequiredContextFields[0] = "mutated" + first[regionIndex].ProviderRegions[0] = "mutated" + first[subtypeIndex].VerifiableSubtypes[0] = "mutated" + first[subtypeIndex].UnverifiableSubtypes[0] = "mutated" + first[referenceIndex].ContractReferenceURLs[0] = "https://mutated.invalid" + + second := VerificationCapabilities() + assert.Equal(t, originalID, second[0].DetectorID) + assert.Equal(t, originalContext, second[contextIndex].RequiredContextFields[0]) + assert.Equal(t, originalRegion, second[regionIndex].ProviderRegions[0]) + assert.Equal(t, originalVerifiableSubtype, second[subtypeIndex].VerifiableSubtypes[0]) + assert.Equal(t, originalUnverifiableSubtype, second[subtypeIndex].UnverifiableSubtypes[0]) + assert.Equal(t, originalReference, second[referenceIndex].ContractReferenceURLs[0]) +} + +func TestVerificationCapabilities_GitHubOAuthSubtypeContract(t *testing.T) { + for _, capability := range VerificationCapabilities() { + if capability.DetectorID != "github-oauth-token" { + continue + } + assert.Equal(t, []string{"gho", "ghu", "ghs"}, capability.VerifiableSubtypes) + assert.Equal(t, []string{"ghr"}, capability.UnverifiableSubtypes) + return + } + t.Fatal("github-oauth-token capability missing") +} + +func TestVerificationCapabilities_GitLabSubtypeAndInactiveContract(t *testing.T) { + for _, capability := range VerificationCapabilities() { + if capability.DetectorID != "gitlab-pat" { + continue + } + assert.Equal(t, []string{"glpat"}, capability.VerifiableSubtypes) + assert.Equal(t, []string{ + "gldt", "glrt", "glrtr", "glcbt", "glptt", "glimt", "glagent", + "glwt", "glsoat", "glffct", "gloas", "glft", + }, capability.UnverifiableSubtypes) + assert.Equal(t, InactiveTrustedOriginInvalid401, capability.InactiveStatusContract) + return + } + t.Fatal("gitlab-pat capability missing") +} + +func TestVerificationCapabilities_SnykInactiveContractIsTrustedOrigin401Only(t *testing.T) { + for _, capability := range VerificationCapabilities() { + if capability.DetectorID != "snyk-api-key" { + continue + } + assert.Equal(t, InactiveTrustedOriginHTTP401, capability.InactiveStatusContract) + return + } + t.Fatal("snyk-api-key capability missing") +} + +func TestVerificationCapabilities_TwilioAndShopifyAreOperatorContextOnly(t *testing.T) { + want := map[string]struct { + inactive InactiveStatusContract + regions []string + }{ + "shopify-access-token": {inactive: InactiveTrustedStoreHTTP401}, + "twilio-api-key": {inactive: InactiveTrustedOriginHTTP401, regions: []string{"US1", "IE1", "AU1"}}, + } + for _, capability := range VerificationCapabilities() { + expected, ok := want[capability.DetectorID] + if !ok { + continue + } + assert.Equal(t, VerifierRequiresContext, capability.VerifierKind, capability.DetectorID) + assert.Equal(t, EndpointOperatorContextProviderAPI, capability.EndpointClass, capability.DetectorID) + assert.Equal(t, expected.inactive, capability.InactiveStatusContract, capability.DetectorID) + assert.Equal(t, expected.regions, capability.ProviderRegions, capability.DetectorID) + assert.Equal(t, "2026-08-11", capability.LastContractReviewedAt, capability.DetectorID) + delete(want, capability.DetectorID) + } + assert.Empty(t, want) +} + +func TestVerificationCapabilities_CriticalContextContracts(t *testing.T) { + want := map[string]struct { + kind VerifierKind + fields []string + regions []string + }{ + "aws-access-key-id": {VerifierLive, []string{"raw_v2"}, nil}, + "databricks-token": {VerifierLive, []string{"host"}, nil}, + "okta-api-token": {VerifierLive, []string{"domain"}, nil}, + "datadog-api-key": {VerifierRequiresContext, []string{"trusted_api_origin"}, []string{"US1", "US3", "US5", "EU", "AP1", "AP2", "UK1", "US1-FED", "US2-FED"}}, + "github-token": {VerifierRequiresContext, []string{"trusted_api_origin"}, []string{"GitHub.com", "GHES"}}, + "github-oauth-token": {VerifierRequiresContext, []string{"trusted_api_origin"}, []string{"GitHub.com", "GHES"}}, + "snyk-api-key": {VerifierRequiresContext, []string{"trusted_api_origin"}, []string{"US1", "US2", "EU", "AU", "GOV", "PRIVATE"}}, + "grafana-api-key": {VerifierRequiresContext, []string{"trusted_instance_origin"}, nil}, + "shopify-access-token": {VerifierRequiresContext, []string{"trusted_store_origin"}, nil}, + "twilio-api-key": {VerifierRequiresContext, []string{"api_key_sid", "trusted_api_origin"}, []string{"US1", "IE1", "AU1"}}, + } + + got := make(map[string]VerificationCapability, len(want)) + for _, capability := range VerificationCapabilities() { + if _, tracked := want[capability.DetectorID]; tracked { + got[capability.DetectorID] = capability + } + } + require.Len(t, got, len(want)) + for detectorID, expected := range want { + capability := got[detectorID] + assert.Equal(t, expected.kind, capability.VerifierKind, detectorID) + assert.Equal(t, expected.fields, capability.RequiredContextFields, detectorID) + assert.Equal(t, expected.regions, capability.ProviderRegions, detectorID) + } +} diff --git a/internal/meta/counts.go b/internal/meta/counts.go index 85812ae..693883d 100644 --- a/internal/meta/counts.go +++ b/internal/meta/counts.go @@ -1,7 +1,10 @@ -// Package meta holds the canonical, human-maintained project counts that are -// published in the README banner, the social-preview image, and the docs. +// Package meta holds canonical project counts and verification-capability +// metadata published in the README banner, social-preview image, and docs. // -// These constants are the single source of truth for the published numbers: +// These constants are the single source of truth for release metadata and +// registry/output counts. +// VerificationCapabilities is the separate source of truth for direct-live, +// context-required, format-only, and no-verifier capability counts: // // - Detectors and Verifiers are guarded at test time against the live // registries (detector.All() / verifier.All()), so adding or removing one @@ -17,12 +20,14 @@ // re-render their PNGs (the re-render command is in each asset's header). package meta +import "strings" + //go:generate go run ./statsgen const ( // Detectors is the number of compile-time registered secret detectors; // it must equal len(detector.All()). - Detectors = 64 + Detectors = 65 // Verifiers is the number of registered verifiers; it must equal // len(verifier.All()). @@ -35,4 +40,23 @@ const ( // OutputFormats is the number of output formats: json, sarif, csv, table, // and github. OutputFormats = 5 + + // OutputFormatList is the canonical user-facing order and spelling used by + // CLI help and documentation contracts. + OutputFormatList = "json, sarif, csv, table, github" ) + +// OutputFormatNames returns the canonical output formats as a fresh slice. +func OutputFormatNames() []string { + return strings.Split(OutputFormatList, ", ") +} + +// IsOutputFormat reports whether name is a canonical output format. +func IsOutputFormat(name string) bool { + for _, format := range OutputFormatNames() { + if name == format { + return true + } + } + return false +} diff --git a/internal/meta/counts_test.go b/internal/meta/counts_test.go index 4a96699..f06769f 100644 --- a/internal/meta/counts_test.go +++ b/internal/meta/counts_test.go @@ -3,8 +3,10 @@ package meta import ( "os" "path/filepath" + "regexp" "strings" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -65,4 +67,43 @@ func TestOutputFormats_MatchOutputPackageDirectories(t *testing.T) { assert.Equal(t, OutputFormats, got, "meta.OutputFormats drifted from the number of internal/output subpackages; "+ "update internal/meta then run `go generate ./...`") + assert.Len(t, OutputFormatNames(), OutputFormats, + "meta.OutputFormatList and meta.OutputFormats must change together") + + seen := make(map[string]struct{}, OutputFormats) + for _, format := range OutputFormatNames() { + assert.Regexp(t, regexp.MustCompile(`^[a-z][a-z0-9-]*$`), format) + if _, duplicate := seen[format]; duplicate { + t.Errorf("duplicate output format %q", format) + } + seen[format] = struct{}{} + assert.True(t, IsOutputFormat(format)) + } + assert.False(t, IsOutputFormat("")) + assert.False(t, IsOutputFormat("unknown")) +} + +func TestReleaseMetadata_MatchesPublishedDocumentation(t *testing.T) { + assert.Regexp(t, regexp.MustCompile(`^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$`), ReleaseVersion) + assert.Regexp(t, regexp.MustCompile(`^[0-9]{4}-[0-9]{2}-[0-9]{2}$`), ReleaseDate) + _, err := time.Parse(time.DateOnly, ReleaseDate) + require.NoError(t, err, "ReleaseDate must be a real ISO 8601 calendar date") + + changelog, err := os.ReadFile(filepath.Join("..", "..", "CHANGELOG.md")) + require.NoError(t, err) + assert.Contains(t, string(changelog), "## ["+ReleaseVersion+"] - "+ReleaseDate, + "release metadata must identify a published changelog entry") + + roadmap, err := os.ReadFile(filepath.Join("..", "..", "docs", "05-ROADMAP.md")) + require.NoError(t, err) + assert.Contains(t, string(roadmap), + "| Phase 9 — Detection Accuracy & FP Reduction | Completed | `"+ReleaseVersion+"` | "+ReleaseDate+" |", + "roadmap release record must match canonical release metadata") + assert.Contains(t, string(roadmap), + "| Phase 10 — Detector Library Expansion | Planned | `v1.9.0` | — |", + "the next minor version must remain assigned to planned Phase 10") + assert.NotContains(t, string(roadmap), + "| Phase 9 — Detection Accuracy & FP Reduction | Planned | `"+ReleaseVersion+"` | — |") + assert.Contains(t, string(roadmap), "| Broaden OpenAI key coverage | Delivered in `v1.7.0` |") + assert.Contains(t, string(roadmap), "| GitHub fine-grained PAT support | Delivered in `v1.7.0` |") } diff --git a/internal/meta/documentation_contract_test.go b/internal/meta/documentation_contract_test.go new file mode 100644 index 0000000..8220507 --- /dev/null +++ b/internal/meta/documentation_contract_test.go @@ -0,0 +1,479 @@ +package meta + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + "regexp" + "sort" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +const releaseVersionTokenPattern = `v[0-9]+\.[0-9]+\.[0-9]+(?:[-_][0-9A-Za-z]+(?:[._-][0-9A-Za-z]+)*)?` + +var ( + releaseVersionToken = regexp.MustCompile(`\b(` + releaseVersionTokenPattern + `)(?:[^0-9A-Za-z._-]|$)`) + releaseVersionTokenExact = regexp.MustCompile(`^` + releaseVersionTokenPattern + `$`) + directLeakwatchPinPatterns = []*regexp.Regexp{ + regexp.MustCompile(`(?i)github\.com/hodetech/leakwatch@(` + releaseVersionTokenPattern + `)(?:[^0-9A-Za-z._-]|$)`), + regexp.MustCompile(`(?i)(?:^|[^0-9A-Za-z/])hodetech/leakwatch@(` + releaseVersionTokenPattern + `)(?:[^0-9A-Za-z._-]|$)`), + regexp.MustCompile(`(?i)ghcr\.io/hodetech/leakwatch:(` + releaseVersionTokenPattern + `)(?:[^0-9A-Za-z._-]|$)`), + regexp.MustCompile(`(?i)LEAKWATCH_VERSION[[:space:]]*[:=][[:space:]]*['\"]?(` + releaseVersionTokenPattern + `)(?:[^0-9A-Za-z._-]|$)`), + regexp.MustCompile(`(?i)leakwatch[[:space:]]+(` + releaseVersionTokenPattern + `)(?:[^0-9A-Za-z._-]|$)`), + regexp.MustCompile(`(?i)leakwatch[[:space:]]+(?:version|sürümü).*?(` + releaseVersionTokenPattern + `)(?:[^0-9A-Za-z._-]|$)`), + } + historicalVersionMarker = regexp.MustCompile(`(?i)`) +) + +type documentationBlock struct { + startLine int + text string + leakwatchImageTags bool +} + +type versionPin struct { + line int + version string +} + +func TestSupplementalGuides_DeclareCanonicalManualBoundary(t *testing.T) { + guides, err := filepath.Glob(filepath.Join("..", "..", "docs", "guides", "*.md")) + require.NoError(t, err) + require.NotEmpty(t, guides) + + for _, path := range guides { + if filepath.Base(path) == "README.md" { + continue + } + contents, readErr := os.ReadFile(path) + require.NoError(t, readErr) + text := string(contents) + assert.Contains(t, text, "> **Documentation role:** Supplemental", path) + assert.Contains(t, text, "(../user-manuals/en/", path) + } +} + +func TestDocumentation_LeakwatchReleasePinsAreCurrentOrExplicitlyHistorical(t *testing.T) { + root := filepath.Join("..", "..") + paths, err := currentDocumentationMarkdownPaths(root) + require.NoError(t, err) + require.NotEmpty(t, paths) + + for _, path := range paths { + contents, readErr := os.ReadFile(path) + require.NoError(t, readErr) + for _, block := range markdownContractBlocks(string(contents)) { + for _, pin := range leakwatchReleasePins(path, block) { + if pin.version == ReleaseVersion || blockMarksHistoricalVersion(block.text, pin.version) { + continue + } + t.Errorf("%s:%d uses stale Leakwatch pin %s; use %s or mark the same logical example with ", + filepath.ToSlash(path), pin.line, pin.version, ReleaseVersion, pin.version) + } + } + } +} + +func TestLeakwatchReleasePins_ContextAndHistoricalMarkerMatrix(t *testing.T) { + tests := []struct { + name string + path string + contents string + wantPins []string + wantLines []int + wantExempt bool + }{ + {name: "environment equals", contents: "LEAKWATCH_VERSION = v1.6.0", wantPins: []string{"v1.6.0"}, wantLines: []int{1}}, + {name: "environment YAML", contents: "LEAKWATCH_VERSION: 'v1.6.0'", wantPins: []string{"v1.6.0"}, wantLines: []int{1}}, + {name: "Leakwatch action input", contents: "```yaml\n- uses: HodeTech/Leakwatch@v1\n with:\n version: v1.6.0\n- uses: actions/setup-node@v4\n with:\n version: v22.3.0\n```", wantPins: []string{"v1.6.0"}, wantLines: []int{4}}, + {name: "exact Leakwatch action pin", contents: "```yaml\n- uses: HodeTech/Leakwatch@v1.6.0\n```", wantPins: []string{"v1.6.0"}, wantLines: []int{2}}, + {name: "Leakwatch pre-commit rev", contents: "```yaml\n- repo: https://github.com/HodeTech/Leakwatch\n rev: v1.6.0\n```", wantPins: []string{"v1.6.0"}, wantLines: []int{3}}, + {name: "unrelated rev in same YAML", contents: "```yaml\n- repo: https://github.com/HodeTech/Leakwatch\n rev: v1.7.0\n- repo: https://example.com/tool\n rev: v9.8.7\n```", wantPins: []string{"v1.7.0"}, wantLines: []int{3}}, + {name: "unrelated version assignment", contents: "```yaml\nname: unrelated\nversion: v2.3.0\n```"}, + {name: "suffix cannot hide behind current stable prefix", contents: "LEAKWATCH_VERSION=v1.7.0-typo", wantPins: []string{"v1.7.0-typo"}, wantLines: []int{1}}, + {name: "contextual image tag table", contents: "```text\nghcr.io/hodetech/leakwatch\n```\n\nAvailable tags:\n\n| Tag | Description |\n|---|---|\n| `:v1.6.0` | stale |", wantPins: []string{"v1.6.0"}, wantLines: []int{9}}, + {name: "bare tag for unrelated image is ignored", contents: "Available tags:\n\n| Tag | Description |\n|---|---|\n| `:v9.8.7` | unrelated |"}, + {name: "exact historical marker", contents: "\n\n```bash\nleakwatch v1.6.0\n```", wantPins: []string{"v1.6.0"}, wantLines: []int{4}, wantExempt: true}, + {name: "marker cannot exempt different pin", contents: "\n\nleakwatch v1.6.0", wantPins: []string{"v1.6.0"}, wantLines: []int{3}}, + {name: "marker cannot exempt later block", contents: "\n\nleakwatch v1.6.0\n\nUnrelated paragraph.\n\nleakwatch v1.6.0", wantPins: []string{"v1.6.0", "v1.6.0"}, wantLines: []int{3, 7}}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var got []string + var gotLines []int + var exemptions []bool + for _, block := range markdownContractBlocks(tc.contents) { + for _, pin := range leakwatchReleasePins(tc.path, block) { + got = append(got, pin.version) + gotLines = append(gotLines, pin.line) + exemptions = append(exemptions, blockMarksHistoricalVersion(block.text, pin.version)) + } + } + assert.Equal(t, tc.wantPins, got) + assert.Equal(t, tc.wantLines, gotLines) + if tc.wantExempt { + assert.Equal(t, []bool{true}, exemptions) + } else if len(exemptions) > 0 { + assert.False(t, exemptions[len(exemptions)-1]) + } + }) + } +} + +func TestEntropyPolicyDocs_MatchEngineContract(t *testing.T) { + root := filepath.Join("..", "..") + oldADR, err := os.ReadFile(filepath.Join(root, "docs", "decisions", "ADR-0005-pattern-matching.md")) + require.NoError(t, err) + assert.Contains(t, string(oldADR), "**Status:** Amended by [ADR-0010]") + + currentADR, err := os.ReadFile(filepath.Join(root, "docs", "decisions", "ADR-0010-entropy-gating-policy.md")) + require.NoError(t, err) + for _, contract := range []string{"EntropyBased", "EntropyGated", "generic-api-key", "structural provider findings", "per-rule `entropy` threshold"} { + assert.Contains(t, string(currentADR), contract) + } + + engineSource, err := os.ReadFile(filepath.Join(root, "internal", "engine", "engine.go")) + require.NoError(t, err) + assert.Contains(t, string(engineSource), "isEntropyGated(det, raw)") + genericSource, err := os.ReadFile(filepath.Join(root, "internal", "detector", "generic", "generic_api_key.go")) + require.NoError(t, err) + assert.Contains(t, string(genericSource), "func (d *APIKeyDetector) EntropyBased() bool { return true }") + assert.Contains(t, string(genericSource), "func (d *APIKeyDetector) EntropyGated(raw detector.RawFinding) bool") + + forbidden := []*regexp.Regexp{ + regexp.MustCompile(`(?is)entropy.{0,120}display-only`), + regexp.MustCompile(`(?is)engine never gates or suppresses.{0,120}entropy`), + regexp.MustCompile(`(?is)global entropy gate.{0,120}(?:planned|not yet implemented)`), + } + for _, dir := range []string{ + filepath.Join(root, "docs", "architecture"), + filepath.Join(root, "docs", "guides"), + filepath.Join(root, "docs", "user-manuals"), + } { + walkErr := filepath.WalkDir(dir, func(path string, entry fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if entry.IsDir() || !strings.EqualFold(filepath.Ext(entry.Name()), ".md") { + return nil + } + contents, readErr := os.ReadFile(path) + if readErr != nil { + return readErr + } + for _, pattern := range forbidden { + assert.False(t, pattern.Match(contents), "%s contains superseded entropy policy matching %s", filepath.ToSlash(path), pattern) + } + return nil + }) + require.NoError(t, walkErr) + } +} + +func TestSlackScopeDocumentation_ContainsExecutableRequiredSet(t *testing.T) { + root := filepath.Join("..", "..") + paths := []string{ + filepath.Join(root, "docs", "guides", "configuration.md"), + filepath.Join(root, "docs", "guides", "slack-scanning.md"), + filepath.Join(root, "docs", "user-manuals", "en", "scanning", "slack.md"), + filepath.Join(root, "docs", "user-manuals", "tr", "scanning", "slack.md"), + filepath.Join(root, "site", "js", "manuals", "en.js"), + filepath.Join(root, "site", "js", "manuals", "tr.js"), + } + scopes := []string{ + "channels:read", "channels:history", "groups:read", "groups:history", + "im:read", "im:history", "mpim:read", "mpim:history", "files:read", + } + for _, path := range paths { + contents, err := os.ReadFile(path) + require.NoError(t, err) + for _, scope := range scopes { + assert.Contains(t, string(contents), scope, "%s is missing %s", filepath.ToSlash(path), scope) + } + } +} + +func TestFindingWireSemanticsDocs_MatchEngine(t *testing.T) { + root := filepath.Join("..", "..") + paths := []string{ + filepath.Join(root, "docs", "user-manuals", "en", "getting-started", "how-it-works.md"), + filepath.Join(root, "docs", "user-manuals", "tr", "getting-started", "how-it-works.md"), + } + for _, path := range paths { + contents, err := os.ReadFile(path) + require.NoError(t, err) + text := string(contents) + assert.Contains(t, text, "decimal(byteOffset)", path) + assert.Contains(t, text, "detection.entropy.enabled", path) + assert.NotContains(t, text, "sha256(detectorID + redacted + filePath + line)", path) + } + for _, path := range []string{ + filepath.Join(root, "docs", "user-manuals", "en", "getting-started", "introduction.md"), + filepath.Join(root, "docs", "user-manuals", "tr", "getting-started", "introduction.md"), + } { + contents, err := os.ReadFile(path) + require.NoError(t, err) + counts := VerificationCapabilityCounts() + assert.Contains(t, string(contents), fmt.Sprintf("%d ", counts.None), + "not-verifiable detector count must come from the capability manifest") + } +} + +func currentDocumentationMarkdownPaths(root string) ([]string, error) { + skipDirs := map[string]struct{}{ + ".git": {}, "node_modules": {}, "vendor": {}, "dist": {}, "review": {}, + } + var paths []string + err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if entry.IsDir() { + if path != root { + if _, skip := skipDirs[entry.Name()]; skip { + return filepath.SkipDir + } + } + return nil + } + if !strings.EqualFold(filepath.Ext(entry.Name()), ".md") { + return nil + } + rel, relErr := filepath.Rel(root, path) + if relErr != nil { + return relErr + } + rel = filepath.ToSlash(rel) + if strings.EqualFold(filepath.Base(rel), "CHANGELOG.md") || rel == "docs/05-ROADMAP.md" { + return nil + } + paths = append(paths, path) + return nil + }) + sort.Strings(paths) + return paths, err +} + +func markdownContractBlocks(contents string) []documentationBlock { + lines := strings.Split(strings.ReplaceAll(contents, "\r\n", "\n"), "\n") + var blocks []documentationBlock + for index := 0; index < len(lines); { + index = nextNonBlankLine(lines, index) + if index >= len(lines) { + break + } + start := index + prefix, contentStart := documentationBlockPrefix(lines, index) + if contentStart >= len(lines) { + blocks = append(blocks, documentationBlock{startLine: start + 1, text: prefix}) + break + } + end := documentationBlockEnd(lines, contentStart) + blocks = append(blocks, documentationBlock{startLine: start + 1, text: prefix + strings.Join(lines[contentStart:end], "\n")}) + index = end + } + markLeakwatchImageTagTables(blocks) + return blocks +} + +func nextNonBlankLine(lines []string, index int) int { + for index < len(lines) && strings.TrimSpace(lines[index]) == "" { + index++ + } + return index +} + +func documentationBlockPrefix(lines []string, index int) (string, int) { + if !historicalVersionMarker.MatchString(lines[index]) { + return "", index + } + contentStart := nextNonBlankLine(lines, index+1) + return strings.Join(lines[index:contentStart], "\n") + "\n", contentStart +} + +func documentationBlockEnd(lines []string, start int) int { + trimmed := strings.TrimSpace(lines[start]) + if strings.HasPrefix(trimmed, "```") || strings.HasPrefix(trimmed, "~~~") { + return fencedDocumentationBlockEnd(lines, start, trimmed[:3]) + } + end := start + 1 + for end < len(lines) && strings.TrimSpace(lines[end]) != "" && !startsMarkdownFence(lines[end]) { + end++ + } + return end +} + +func fencedDocumentationBlockEnd(lines []string, start int, fence string) int { + for end := start + 1; end < len(lines); end++ { + if strings.HasPrefix(strings.TrimSpace(lines[end]), fence) { + return end + 1 + } + } + return len(lines) +} + +func startsMarkdownFence(line string) bool { + trimmed := strings.TrimSpace(line) + return strings.HasPrefix(trimmed, "```") || strings.HasPrefix(trimmed, "~~~") +} + +func markLeakwatchImageTagTables(blocks []documentationBlock) { + pendingTagTable := false + for index := range blocks { + trimmed := strings.TrimSpace(blocks[index].text) + if strings.HasPrefix(trimmed, "#") { + pendingTagTable = false + } + if strings.Contains(strings.ToLower(blocks[index].text), "ghcr.io/hodetech/leakwatch") { + pendingTagTable = true + } + if pendingTagTable && strings.HasPrefix(trimmed, "|") && strings.Contains(blocks[index].text, "`:v") { + blocks[index].leakwatchImageTags = true + pendingTagTable = false + } + } +} + +func leakwatchReleasePins(_ string, block documentationBlock) []versionPin { + byVersionAndLine := make(map[versionPin]struct{}) + collect := func(pattern *regexp.Regexp) { + for _, match := range pattern.FindAllStringSubmatchIndex(block.text, -1) { + if len(match) < 4 || match[2] < 0 { + continue + } + version := block.text[match[2]:match[3]] + line := block.startLine + strings.Count(block.text[:match[2]], "\n") + byVersionAndLine[versionPin{line: line, version: version}] = struct{}{} + } + } + for _, pattern := range directLeakwatchPinPatterns { + collect(pattern) + } + for _, pin := range leakwatchYAMLPins(block) { + byVersionAndLine[pin] = struct{}{} + } + + if block.leakwatchImageTags { + for _, location := range releaseVersionToken.FindAllStringSubmatchIndex(block.text, -1) { + if len(location) < 4 || location[2] < 0 { + continue + } + version := block.text[location[2]:location[3]] + line := block.startLine + strings.Count(block.text[:location[2]], "\n") + byVersionAndLine[versionPin{line: line, version: version}] = struct{}{} + } + } + + pins := make([]versionPin, 0, len(byVersionAndLine)) + for pin := range byVersionAndLine { + pins = append(pins, pin) + } + sort.Slice(pins, func(i, j int) bool { + if pins[i].line == pins[j].line { + return pins[i].version < pins[j].version + } + return pins[i].line < pins[j].line + }) + return pins +} + +func leakwatchYAMLPins(block documentationBlock) []versionPin { + source, lineOffset := yamlSourceFromBlock(block.text) + if strings.TrimSpace(source) == "" { + return nil + } + var document yaml.Node + if err := yaml.Unmarshal([]byte(source), &document); err != nil { + return nil + } + var pins []versionPin + var walk func(*yaml.Node) + walk = func(node *yaml.Node) { + if node.Kind == yaml.MappingNode { + values := yamlMappingValues(node) + if repo := values["repo"]; repo != nil && isLeakwatchRepository(repo.Value) { + if rev := values["rev"]; rev != nil { + if version := releaseVersionTokenExact.FindString(rev.Value); version != "" { + pins = append(pins, versionPin{ + line: block.startLine + lineOffset + rev.Line - 1, + version: version, + }) + } + } + } + if uses := values["uses"]; uses != nil && isLeakwatchAction(uses.Value) { + if with := values["with"]; with != nil && with.Kind == yaml.MappingNode { + if versionNode := yamlMappingValues(with)["version"]; versionNode != nil { + if version := releaseVersionTokenExact.FindString(versionNode.Value); version != "" { + pins = append(pins, versionPin{ + line: block.startLine + lineOffset + versionNode.Line - 1, + version: version, + }) + } + } + } + } + } + for _, child := range node.Content { + walk(child) + } + } + walk(&document) + return pins +} + +func yamlSourceFromBlock(block string) (string, int) { + lines := strings.Split(block, "\n") + for index, line := range lines { + trimmed := strings.ToLower(strings.TrimSpace(line)) + if !strings.HasPrefix(trimmed, "```yaml") && !strings.HasPrefix(trimmed, "```yml") && + !strings.HasPrefix(trimmed, "~~~yaml") && !strings.HasPrefix(trimmed, "~~~yml") { + continue + } + fence := strings.TrimSpace(line)[:3] + for end := index + 1; end < len(lines); end++ { + if strings.HasPrefix(strings.TrimSpace(lines[end]), fence) { + return strings.Join(lines[index+1:end], "\n"), index + 1 + } + } + return "", 0 + } + return block, 0 +} + +func yamlMappingValues(mapping *yaml.Node) map[string]*yaml.Node { + values := make(map[string]*yaml.Node, len(mapping.Content)/2) + for index := 0; index+1 < len(mapping.Content); index += 2 { + key := mapping.Content[index] + if key.Kind == yaml.ScalarNode { + values[strings.ToLower(strings.TrimSpace(key.Value))] = mapping.Content[index+1] + } + } + return values +} + +func isLeakwatchRepository(value string) bool { + normalized := strings.TrimSuffix(strings.ToLower(strings.TrimSpace(value)), ".git") + return normalized == "https://github.com/hodetech/leakwatch" || normalized == "github.com/hodetech/leakwatch" +} + +func isLeakwatchAction(value string) bool { + normalized := strings.ToLower(strings.TrimSpace(value)) + return strings.HasPrefix(normalized, "hodetech/leakwatch@") || + strings.HasPrefix(normalized, "github.com/hodetech/leakwatch@") +} + +func blockMarksHistoricalVersion(block, version string) bool { + for _, match := range historicalVersionMarker.FindAllStringSubmatch(block, -1) { + if strings.EqualFold(match[1], version) { + return true + } + } + return false +} diff --git a/internal/meta/provider_contract_links_test.go b/internal/meta/provider_contract_links_test.go new file mode 100644 index 0000000..95dc49f --- /dev/null +++ b/internal/meta/provider_contract_links_test.go @@ -0,0 +1,124 @@ +package meta + +import ( + "context" + "fmt" + "io" + "net/http" + "net/url" + "os" + "regexp" + "sort" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const providerLinkCheckEnvironment = "LEAKWATCH_CHECK_PROVIDER_LINKS" + +var officialProviderDocumentationHosts = map[string]struct{}{ + "api.slack.com": {}, + "auth0.com": {}, + "docs.datadoghq.com": {}, + "docs.github.com": {}, + "docs.gitlab.com": {}, + "docs.newrelic.com": {}, + "docs.slack.dev": {}, + "docs.snyk.io": {}, + "github.blog": {}, + "grafana.com": {}, + "shopify.dev": {}, + "supabase.com": {}, + "www.twilio.com": {}, +} + +func isOfficialProviderDocumentationURL(raw string) bool { + parsed, err := url.Parse(raw) + if err != nil || parsed.Scheme != "https" || parsed.User != nil || parsed.Hostname() == "" { + return false + } + _, ok := officialProviderDocumentationHosts[strings.ToLower(parsed.Hostname())] + return ok +} + +func TestVerificationCapabilities_ReferencesUseOfficialProviderHosts(t *testing.T) { + for _, capability := range VerificationCapabilities() { + for _, reference := range capability.ContractReferenceURLs { + assert.True(t, isOfficialProviderDocumentationURL(reference), "%s: %s", capability.DetectorID, reference) + } + } + assert.False(t, isOfficialProviderDocumentationURL("https://docs.gitlab.com.attacker.example/api/users/")) + assert.False(t, isOfficialProviderDocumentationURL("http://docs.gitlab.com/api/users/")) +} + +func TestVerificationCapabilities_CriticalReferencesMatchImplementedEndpoints(t *testing.T) { + byDetector := make(map[string][]string) + for _, capability := range VerificationCapabilities() { + byDetector[capability.DetectorID] = capability.ContractReferenceURLs + } + assert.Contains(t, byDetector["gitlab-pat"], "https://docs.gitlab.com/api/users/#retrieve-the-current-user") + assert.Contains(t, byDetector["gitlab-pat"], "https://docs.gitlab.com/api/personal_access_tokens/") + assert.Contains(t, byDetector["github-oauth-token"], "https://docs.github.com/en/rest/apps/installations#list-repositories-accessible-to-the-app-installation") +} + +func TestVerificationCapabilities_ReviewedReferenceLinksResolve(t *testing.T) { + if os.Getenv(providerLinkCheckEnvironment) != "1" { + t.Skip("network contract audit is enabled only by the protected scheduled workflow") + } + + client := &http.Client{ + Timeout: 20 * time.Second, + CheckRedirect: func(request *http.Request, via []*http.Request) error { + if len(via) >= 5 { + return fmt.Errorf("provider documentation redirect limit exceeded") + } + if !isOfficialProviderDocumentationURL(request.URL.String()) { + return fmt.Errorf("provider documentation redirected outside official hosts") + } + return nil + }, + } + + seen := make(map[string]struct{}) + var references []string + for _, capability := range VerificationCapabilities() { + for _, reference := range capability.ContractReferenceURLs { + if _, exists := seen[reference]; !exists { + seen[reference] = struct{}{} + references = append(references, reference) + } + } + } + sort.Strings(references) + for _, reference := range references { + t.Run(reference, func(t *testing.T) { + parsed, err := url.Parse(reference) + require.NoError(t, err) + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + request, err := http.NewRequestWithContext(ctx, http.MethodGet, reference, nil) + require.NoError(t, err) + request.Header.Set("User-Agent", "leakwatch-provider-contract-audit") + response, err := client.Do(request) + require.NoError(t, err) + defer response.Body.Close() + require.GreaterOrEqual(t, response.StatusCode, http.StatusOK) + require.Less(t, response.StatusCode, http.StatusMultipleChoices) + + body, err := io.ReadAll(io.LimitReader(response.Body, 8*1024*1024+1)) + require.NoError(t, err) + require.LessOrEqual(t, len(body), 8*1024*1024, "provider documentation response exceeds audit bound") + if parsed.Fragment == "" { + return + } + fragment, err := url.PathUnescape(parsed.Fragment) + require.NoError(t, err) + fragmentPattern := regexp.MustCompile(`(?i)\bid=(?:"` + regexp.QuoteMeta(fragment) + `"|'` + regexp.QuoteMeta(fragment) + `'|` + regexp.QuoteMeta(fragment) + `(?:[ >]))`) + require.True(t, fragmentPattern.Match(body), + "fragment %q is absent from provider documentation", fragment) + }) + } +} diff --git a/internal/meta/release.go b/internal/meta/release.go new file mode 100644 index 0000000..22c9bc1 --- /dev/null +++ b/internal/meta/release.go @@ -0,0 +1,11 @@ +package meta + +const ( + // ReleaseVersion is the latest stable release published to users. Release + // tooling still injects the running binary's exact build version; this value + // owns static product surfaces such as the website footer and roadmap guards. + ReleaseVersion = "v1.8.0" + + // ReleaseDate is the publication date of ReleaseVersion (ISO 8601). + ReleaseDate = "2026-08-11" +) diff --git a/internal/meta/releasecheck/main.go b/internal/meta/releasecheck/main.go new file mode 100644 index 0000000..083e6c9 --- /dev/null +++ b/internal/meta/releasecheck/main.go @@ -0,0 +1,74 @@ +// Command releasecheck rejects malformed release tags and prevents a stable +// tag from publishing artifacts whose metadata still names another release. +package main + +import ( + "flag" + "fmt" + "os" + "regexp" + "strings" + + "github.com/HodeTech/leakwatch/internal/meta" +) + +var ( + stableTagPattern = regexp.MustCompile(`^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$`) + prereleaseIdentifierRule = regexp.MustCompile(`^[0-9A-Za-z-]+$`) +) + +func main() { + tag := flag.String("tag", "", "Git release tag being published") + flag.Parse() + if flag.NArg() != 0 { + fail(fmt.Errorf("unexpected positional arguments")) + } + prerelease, err := validateReleaseTag(*tag) + if err != nil { + fail(err) + } + if prerelease { + fmt.Printf("prerelease tag %s is valid; stable metadata remains %s\n", *tag, meta.ReleaseVersion) + return + } + fmt.Printf("release metadata matches stable tag %s\n", *tag) +} + +func validateReleaseTag(tag string) (bool, error) { + base, prerelease, hasPrerelease := strings.Cut(tag, "-") + if !stableTagPattern.MatchString(base) { + return false, fmt.Errorf("release tag %q does not start with a valid vMAJOR.MINOR.PATCH version", tag) + } + if !hasPrerelease { + if tag != meta.ReleaseVersion { + return false, fmt.Errorf("release tag %q does not match internal/meta.ReleaseVersion %q", tag, meta.ReleaseVersion) + } + return false, nil + } + if prerelease == "" { + return false, fmt.Errorf("release tag %q has an empty prerelease suffix", tag) + } + for _, identifier := range strings.Split(prerelease, ".") { + if !prereleaseIdentifierRule.MatchString(identifier) { + return false, fmt.Errorf("release tag %q has invalid prerelease identifier %q", tag, identifier) + } + if len(identifier) > 1 && identifier[0] == '0' && isDecimal(identifier) { + return false, fmt.Errorf("release tag %q has a numeric prerelease identifier with a leading zero", tag) + } + } + return true, nil +} + +func isDecimal(value string) bool { + for _, r := range value { + if r < '0' || r > '9' { + return false + } + } + return true +} + +func fail(err error) { + fmt.Fprintln(os.Stderr, "releasecheck:", err) + os.Exit(1) +} diff --git a/internal/meta/releasecheck/main_test.go b/internal/meta/releasecheck/main_test.go new file mode 100644 index 0000000..ad5a5ab --- /dev/null +++ b/internal/meta/releasecheck/main_test.go @@ -0,0 +1,53 @@ +package main + +import ( + "testing" + + "github.com/HodeTech/leakwatch/internal/meta" +) + +func TestValidateStableReleaseTag(t *testing.T) { + prerelease, err := validateReleaseTag(meta.ReleaseVersion) + if err != nil { + t.Fatalf("validateReleaseTag(%q) error = %v", meta.ReleaseVersion, err) + } + if prerelease { + t.Fatalf("validateReleaseTag(%q) prerelease = true, want false", meta.ReleaseVersion) + } +} + +func TestValidateReleaseTag_AcceptsValidPrereleases(t *testing.T) { + for _, tag := range []string{"v1.8.0-rc.1", "v2.0.0-beta.2", "v2.0.0-preview-x"} { + t.Run(tag, func(t *testing.T) { + prerelease, err := validateReleaseTag(tag) + if err != nil { + t.Fatalf("validateReleaseTag(%q) error = %v", tag, err) + } + if !prerelease { + t.Fatalf("validateReleaseTag(%q) prerelease = false, want true", tag) + } + }) + } +} + +func TestValidateReleaseTag_FailsClosed(t *testing.T) { + for _, tag := range []string{ + "", + "vnext", + "1.7.0", + "v01.7.0", + "v1.7", + "v999.0.0", + "v1.8.0-", + "v1.8.0-rc..1", + "v1.8.0-rc+build", + "v1.8.0-01", + "v1.8.0_rc1", + } { + t.Run(tag, func(t *testing.T) { + if _, err := validateReleaseTag(tag); err == nil { + t.Fatalf("validateReleaseTag(%q) error = nil, want fail-closed error", tag) + } + }) + } +} diff --git a/internal/meta/statsgen/main.go b/internal/meta/statsgen/main.go index 9337e69..e2879e8 100644 --- a/internal/meta/statsgen/main.go +++ b/internal/meta/statsgen/main.go @@ -28,6 +28,7 @@ const ( // managedFiles are rewritten relative to the repository root. var managedFiles = []string{ "docs/assets/banner.html", + "docs/assets/banner.svg", "site/assets/og.svg", } @@ -40,9 +41,10 @@ type replacement struct { } func replacements() []replacement { + capabilities := meta.VerificationCapabilityCounts() return []replacement{ {regexp.MustCompile(`\d+ detectors`), fmt.Sprintf("%d detectors", meta.Detectors)}, - {regexp.MustCompile(`\d+ live verifiers`), fmt.Sprintf("%d live verifiers", meta.Verifiers)}, + {regexp.MustCompile(`\d+ (?:live verifiers|direct-live checks)`), fmt.Sprintf("%d direct-live checks", capabilities.Live)}, {regexp.MustCompile(`\d+ sources`), fmt.Sprintf("%d sources", meta.Sources)}, {regexp.MustCompile(`\d+ output formats`), fmt.Sprintf("%d output formats", meta.OutputFormats)}, } @@ -82,7 +84,7 @@ func main() { } if len(stale) > 0 { - fail(fmt.Errorf("stat blocks out of date in: %s\nrun `go generate ./...` and re-render the PNGs", + fail(fmt.Errorf("stat blocks out of date in: %s\nrun `go generate ./...`", strings.Join(stale, ", "))) } } diff --git a/internal/meta/statsgen/main_test.go b/internal/meta/statsgen/main_test.go index 9e3129e..5b7d069 100644 --- a/internal/meta/statsgen/main_test.go +++ b/internal/meta/statsgen/main_test.go @@ -8,8 +8,9 @@ import ( // TestManagedAssetsUpToDate fails when a marketing asset's stat block no longer // matches internal/meta — i.e. a count was bumped but `go generate ./...` was -// not run (or the PNG was re-rendered from a stale source). It runs as part of -// `go test ./...`, so CI catches the drift without a dedicated workflow step. +// not run. README renders the directly managed SVG, so the visible banner is +// covered without a browser-dependent rasterization step. It runs as part of +// `go test ./...`, so CI catches source drift without a dedicated workflow step. func TestManagedAssetsUpToDate(t *testing.T) { root, err := repoRoot() if err != nil { @@ -25,7 +26,19 @@ func TestManagedAssetsUpToDate(t *testing.T) { t.Fatalf("%s: %v", rel, err) } if updated != string(orig) { - t.Errorf("%s stat block is stale; run `go generate ./...` and re-render its PNG", rel) + t.Errorf("%s stat block is stale; run `go generate ./...`", rel) } } } + +func TestRewriteMigratesLegacyLiveVerifierLabel(t *testing.T) { + input := "54 live verifiers" + got, err := rewrite(input) + if err != nil { + t.Fatalf("rewrite legacy label: %v", err) + } + want := "39 direct-live checks" + if got != want { + t.Fatalf("rewrite legacy label = %q, want %q", got, want) + } +} diff --git a/internal/meta/vscode_release_contract_test.go b/internal/meta/vscode_release_contract_test.go new file mode 100644 index 0000000..f734ac7 --- /dev/null +++ b/internal/meta/vscode_release_contract_test.go @@ -0,0 +1,39 @@ +package meta + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestVSCodeMarketplaceWorkflow_FailsClosedOnRepositoryPolicy(t *testing.T) { + path := filepath.Join("..", "..", ".github", "workflows", "vscode-release.yml") + contents, err := os.ReadFile(path) + require.NoError(t, err) + workflow := string(contents) + + for _, contract := range []string{ + "inputs.publish && github.ref == 'refs/heads/main'", + "environment: vscode-marketplace", + "actions: read", + "deployments: read", + "prevent_self_review == true", + "deployment_branch_policy.protected_branches == true", + "VSCODE_ENV_AUDIT_TOKEN", + "${{ secrets.VSCODE_ENV_AUDIT_TOKEN }}", + "@<(printf 'Authorization: Bearer %s\\n' \"${GH_ENVIRONMENT_AUDIT_TOKEN:?}\")", + "environments/vscode-marketplace/secrets/VSCE_PAT", + ".name == \"VSCE_PAT\"", + "${{ secrets.VSCE_PAT }}", + "${VSCE_PAT:?missing environment-scoped VSCE_PAT}", + "npx --no-install vsce publish --packagePath", + "if [[ ${#packages[@]} -ne 1 ]]", + } { + assert.Contains(t, workflow, contract) + } + assert.NotContains(t, workflow, "--pat") + assert.NotContains(t, workflow, "Authorization: Bearer ${", "tokens must not be exposed in process arguments") +} diff --git a/internal/output/formatter.go b/internal/output/formatter.go index 48f6506..0450aef 100644 --- a/internal/output/formatter.go +++ b/internal/output/formatter.go @@ -3,6 +3,7 @@ package output import ( + "fmt" "io" "strings" "unicode" @@ -27,7 +28,9 @@ type Formatter interface { // SanitizeForDisplay strips Unicode control characters — including the ESC // (0x1B) byte that begins ANSI/OSC/DCS escape sequences, and the C0/C1 -// control ranges more generally — from s. +// control ranges more generally — from s. Unicode bidi controls are rendered +// as visible \\uXXXX escapes so they cannot reorder terminal text while their +// presence and exact code point remain available to the operator. // // Apply this to any attacker-influenced field (file path, detector ID, // redacted match value, ...) before it is written somewhere an operator @@ -38,20 +41,38 @@ type Formatter interface { // custom-rule match can inject cursor moves, color changes, title-bar // writes, or other terminal escape sequences into the operator's session. // -// Bytes are removed rather than substituted: the safe behavior for a -// terminal-facing display field is to drop the disallowed byte entirely -// rather than risk re-introducing structure (e.g. a substitute delimiter) -// that could itself be misinterpreted downstream. +// Control bytes are removed rather than substituted: the safe behavior for a +// terminal-facing display field is to drop them entirely rather than risk +// re-introducing structure (e.g. a substitute delimiter) that could itself be +// misinterpreted downstream. Bidi controls are escaped instead because their +// code-point identity is useful evidence and the visible ASCII form is inert. // // Do NOT apply this to the unredacted secret value (Raw) shown under // --show-raw: that field's entire purpose is exact fidelity to the actual // secret, and silently dropping bytes from it would misrepresent the secret // being reported. func SanitizeForDisplay(s string) string { - return strings.Map(func(r rune) rune { + var sanitized strings.Builder + for _, r := range s { if unicode.IsControl(r) { - return -1 + continue } - return r - }, s) + if isBidiControl(r) { + _, _ = fmt.Fprintf(&sanitized, "\\u%04X", r) + continue + } + sanitized.WriteRune(r) + } + return sanitized.String() +} + +// isBidiControl covers the Unicode bidi controls that can alter the visual +// order of surrounding text. Other format characters, including emoji ZWJ, +// remain intact so ordinary internationalized paths are not degraded. +func isBidiControl(r rune) bool { + return r == '\u061C' || + r == '\u200E' || + r == '\u200F' || + (r >= '\u202A' && r <= '\u202E') || + (r >= '\u2066' && r <= '\u2069') } diff --git a/internal/output/formatter_test.go b/internal/output/formatter_test.go index 9e0d712..5f4e57c 100644 --- a/internal/output/formatter_test.go +++ b/internal/output/formatter_test.go @@ -53,6 +53,16 @@ func TestSanitizeForDisplay(t *testing.T) { in: "config-é.yaml", want: "config-é.yaml", }, + { + name: "bidi controls are visibly escaped without reordering text", + in: "safe.go\u202ecod.exe\u2066tail", + want: "safe.go\\u202Ecod.exe\\u2066tail", + }, + { + name: "emoji ZWJ and combining marks are preserved", + in: "family-👩\u200d💻-cafe\u0301", + want: "family-👩\u200d💻-cafe\u0301", + }, } for _, tt := range tests { diff --git a/internal/output/json/json_formatter.go b/internal/output/json/json_formatter.go index eb626f2..fc29133 100644 --- a/internal/output/json/json_formatter.go +++ b/internal/output/json/json_formatter.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io" + "time" "github.com/HodeTech/leakwatch/pkg/finding" ) @@ -26,10 +27,14 @@ type Formatter struct { // gated the same way Raw is: it must never carry secret material (see // finding.Finding's doc comment), but it is still opt-in behind --show-raw // alongside Raw so it does not appear in default output either. +type findingAlias finding.Finding + type findingJSON struct { - finding.Finding - Raw string `json:"raw,omitempty"` - ExtraData map[string]string `json:"extra_data,omitempty"` + findingAlias + Raw string `json:"raw,omitempty"` + ExtraData map[string]string `json:"extra_data,omitempty"` + DetectedAt *time.Time `json:"detected_at,omitempty"` + Entropy *float64 `json:"entropy,omitempty"` } // Format writes findings as JSON to the given writer. @@ -50,7 +55,14 @@ func (f *Formatter) Format(w io.Writer, findings []finding.Finding) error { output := make([]findingJSON, len(findings)) for i, fd := range findings { - output[i] = findingJSON{Finding: fd, Raw: fd.Raw, ExtraData: fd.ExtraData} + wire := findingJSON{findingAlias: findingAlias(fd), Raw: fd.Raw, ExtraData: fd.ExtraData} + if !fd.DetectedAt.IsZero() { + wire.DetectedAt = &fd.DetectedAt + } + if fd.EntropyCalculated || fd.Entropy != 0 { + wire.Entropy = &fd.Entropy + } + output[i] = wire } if err := encoder.Encode(output); err != nil { return fmt.Errorf("failed to write JSON output: %w", err) diff --git a/internal/output/json/json_formatter_test.go b/internal/output/json/json_formatter_test.go index fd2b194..adaff3d 100644 --- a/internal/output/json/json_formatter_test.go +++ b/internal/output/json/json_formatter_test.go @@ -223,6 +223,21 @@ func TestFormatter_Format_ShowRawTrue_OmitsExtraDataWhenNil(t *testing.T) { assert.False(t, hasExtraData, "extra_data should be omitted when nil, even with ShowRaw=true") } +func TestFormatter_Format_ShowRaw_PreservesNullableFindingFields(t *testing.T) { + formatted := finding.Finding{ID: "computed-zero", Raw: "synthetic-secret"} + formatted.SetEntropy(0) + + var buf bytes.Buffer + require.NoError(t, (&Formatter{ShowRaw: true}).Format(&buf, []finding.Finding{formatted})) + + var output []map[string]any + require.NoError(t, json.Unmarshal(buf.Bytes(), &output)) + require.Len(t, output, 1) + assert.NotContains(t, output[0], "detected_at") + assert.Equal(t, float64(0), output[0]["entropy"]) + assert.Equal(t, "synthetic-secret", output[0]["raw"]) +} + func TestFormatter_Format_ShowRawFalse_DoesNotMutateOriginal(t *testing.T) { f := &Formatter{ShowRaw: false} var buf bytes.Buffer diff --git a/internal/output/table/table_formatter.go b/internal/output/table/table_formatter.go index 5470db6..32195f2 100644 --- a/internal/output/table/table_formatter.go +++ b/internal/output/table/table_formatter.go @@ -6,7 +6,8 @@ import ( "io" "strconv" "strings" - "text/tabwriter" + + "github.com/mattn/go-runewidth" "github.com/HodeTech/leakwatch/internal/output" "github.com/HodeTech/leakwatch/pkg/finding" @@ -34,7 +35,9 @@ type Formatter struct { // Format writes findings as a formatted table to the given writer. // Columns: SEVERITY | DETECTOR | FILE | LINE | REDACTED | STATUS | REMEDIATION -// When ShowRaw is true, a trailing RAW column is appended. +// When ShowRaw is true, a trailing RAW column is appended. Its value is a +// reversible Go-quoted string so arbitrary secret bytes cannot inject terminal +// controls and can still be recovered exactly with strconv.Unquote. // A summary line is appended at the bottom. // // DetectorID, FilePath, and Redacted are attacker-influenced (a malicious @@ -44,32 +47,20 @@ type Formatter struct { // strips control/ANSI-escape bytes so a crafted finding cannot inject // terminal escape sequences into the operator's session. func (f *Formatter) Format(w io.Writer, findings []finding.Finding) error { - tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0) - - // Write header. - header := "SEVERITY\tDETECTOR\tFILE\tLINE\tREDACTED\tSTATUS\tREMEDIATION" - separator := "--------\t--------\t----\t----\t--------\t------\t-----------" + headers := []string{"SEVERITY", "DETECTOR", "FILE", "LINE", "REDACTED", "STATUS", "REMEDIATION"} if f.ShowRaw { - header += "\tRAW" - separator += "\t---" - } - if _, err := fmt.Fprintln(tw, header); err != nil { - return fmt.Errorf("failed to write table header: %w", err) + headers = append(headers, "RAW") } - // Write separator. - if _, err := fmt.Fprintln(tw, separator); err != nil { - return fmt.Errorf("failed to write table separator: %w", err) - } - - // Write rows. + rows := make([]tableRow, 0, len(findings)) for _, fd := range findings { remediation := "-" if fd.Remediation != nil && fd.Remediation.Title != "" { remediation = fd.Remediation.Title } - sevText := strings.ToUpper(fd.Severity.String()) + plainSeverity := strings.ToUpper(fd.Severity.String()) + sevText := plainSeverity if f.ColorEnabled { sevText = f.colorizeSeverity(fd.Severity, sevText) } @@ -79,26 +70,23 @@ func (f *Formatter) Format(w io.Writer, findings []finding.Finding) error { lineNo = strconv.Itoa(fd.SourceMetadata.Line) } - line := fmt.Sprintf( - "%s\t%s\t%s\t%s\t%s\t%s\t%s", - sevText, - output.SanitizeForDisplay(fd.DetectorID), - output.SanitizeForDisplay(fd.SourceMetadata.FilePath), - lineNo, - output.SanitizeForDisplay(fd.Redacted), - fd.Verification.Status.String(), - remediation, - ) - if f.ShowRaw { - line += "\t" + fd.Raw + cells := []tableCell{ + {text: sevText, widthText: plainSeverity}, + {text: output.SanitizeForDisplay(fd.DetectorID)}, + {text: output.SanitizeForDisplay(fd.SourceMetadata.FilePath)}, + {text: lineNo}, + {text: output.SanitizeForDisplay(fd.Redacted)}, + {text: fd.Verification.Status.String()}, + {text: output.SanitizeForDisplay(remediation)}, } - if _, err := fmt.Fprintln(tw, line); err != nil { - return fmt.Errorf("failed to write table row: %w", err) + if f.ShowRaw { + cells = append(cells, tableCell{text: quoteRawForTable(fd.Raw)}) } + rows = append(rows, tableRow{cells: cells}) } - if err := tw.Flush(); err != nil { - return fmt.Errorf("failed to flush table output: %w", err) + if err := writeDisplayWidthTable(w, headers, rows); err != nil { + return err } // Write summary line. @@ -113,6 +101,87 @@ func (f *Formatter) Format(w io.Writer, findings []finding.Finding) error { return nil } +func quoteRawForTable(raw string) string { + return strconv.Quote(raw) +} + +const columnPadding = 2 + +type tableCell struct { + text string + widthText string +} + +func (c tableCell) displayWidth() int { + if c.widthText != "" { + return runewidth.StringWidth(c.widthText) + } + return runewidth.StringWidth(c.text) +} + +type tableRow struct { + cells []tableCell +} + +// writeDisplayWidthTable aligns columns by terminal cells, not bytes or rune +// count. This keeps CJK, emoji, and combining-character paths aligned while +// allowing ANSI-colored severity text to use its uncolored widthText. +func writeDisplayWidthTable(w io.Writer, headers []string, rows []tableRow) error { + widths := make([]int, len(headers)) + for index, header := range headers { + widths[index] = runewidth.StringWidth(header) + } + for _, row := range rows { + for index, cell := range row.cells { + if index < len(widths) && cell.displayWidth() > widths[index] { + widths[index] = cell.displayWidth() + } + } + } + + headerCells := make([]tableCell, len(headers)) + separatorCells := make([]tableCell, len(headers)) + for index, header := range headers { + headerCells[index] = tableCell{text: header} + separatorCells[index] = tableCell{text: strings.Repeat("-", widths[index])} + } + if err := writeTableRow(w, tableRow{cells: headerCells}, widths); err != nil { + return fmt.Errorf("failed to write table header: %w", err) + } + if err := writeTableRow(w, tableRow{cells: separatorCells}, widths); err != nil { + return fmt.Errorf("failed to write table separator: %w", err) + } + for _, row := range rows { + if err := writeTableRow(w, row, widths); err != nil { + return fmt.Errorf("failed to write table row: %w", err) + } + } + return nil +} + +func writeTableRow(w io.Writer, row tableRow, widths []int) error { + for index, cell := range row.cells { + if index > 0 { + if _, err := io.WriteString(w, strings.Repeat(" ", columnPadding)); err != nil { + return err + } + } + if _, err := io.WriteString(w, cell.text); err != nil { + return err + } + if index < len(row.cells)-1 { + padding := widths[index] - cell.displayWidth() + if padding > 0 { + if _, err := io.WriteString(w, strings.Repeat(" ", padding)); err != nil { + return err + } + } + } + } + _, err := io.WriteString(w, "\n") + return err +} + // colorizeSeverity wraps the severity text with the appropriate ANSI color code. func (f *Formatter) colorizeSeverity(sev finding.Severity, text string) string { var color string diff --git a/internal/output/table/table_formatter_test.go b/internal/output/table/table_formatter_test.go index 0a26a60..e48bbc3 100644 --- a/internal/output/table/table_formatter_test.go +++ b/internal/output/table/table_formatter_test.go @@ -3,11 +3,14 @@ package table import ( "bytes" "errors" + "strconv" "strings" "testing" "time" + "unicode/utf8" "github.com/HodeTech/leakwatch/pkg/finding" + "github.com/mattn/go-runewidth" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -138,6 +141,35 @@ func TestFormatter_Format_ShowRawTrue_AddsRawColumn(t *testing.T) { "ShowRaw=true must include the raw secret value in table output") } +func TestFormatter_Format_ShowRawTrue_UsesReversibleTerminalSafeEncoding(t *testing.T) { + raw := "abc\ndef\tghi\x1b\x00\\suffix\u0301\u202e" + string([]byte{0xff}) + encoded := quoteRawForTable(raw) + + recovered, err := strconv.Unquote(encoded) + require.NoError(t, err) + assert.Equal(t, []byte(raw), []byte(recovered), "quoted RAW value must round-trip byte-for-byte") + assert.True(t, utf8.ValidString(encoded), "table display must remain valid UTF-8") + for _, unsafe := range []string{"\n", "\t", "\x1b", "\x00", "\u202e"} { + assert.NotContains(t, encoded, unsafe, "encoded RAW must not contain literal terminal controls") + } + + f := &Formatter{ShowRaw: true} + var buf bytes.Buffer + require.NoError(t, f.Format(&buf, []finding.Finding{{ + DetectorID: "generic-secret", + Raw: raw, + }})) + assert.Contains(t, buf.String(), encoded) +} + +func TestTableCell_DisplayWidthUsesExplicitUncoloredText(t *testing.T) { + cell := tableCell{text: "\x1b[31mCRITICAL\x1b[0m", widthText: "CRITICAL"} + assert.Equal(t, 8, cell.displayWidth(), "ANSI decoration must not contribute to table alignment") + + unicodeCell := tableCell{text: "ignored", widthText: "秘密"} + assert.Equal(t, 4, unicodeCell.displayWidth(), "widthText must retain terminal-cell semantics") +} + func TestFormatter_Format_ShowRawFalse_DoesNotMutateOriginal(t *testing.T) { f := &Formatter{ShowRaw: false} var buf bytes.Buffer @@ -156,7 +188,7 @@ func TestFormatter_Format_ShowRawFalse_DoesNotMutateOriginal(t *testing.T) { "Format must not mutate the original slice") } -func TestFormatter_Format_ColumnsAligned_TabwriterProducesAlignedOutput(t *testing.T) { +func TestFormatter_Format_ColumnsAligned_ProducesAlignedOutput(t *testing.T) { f := &Formatter{} var buf bytes.Buffer @@ -173,6 +205,48 @@ func TestFormatter_Format_ColumnsAligned_TabwriterProducesAlignedOutput(t *testi require.GreaterOrEqual(t, len(lines), 4) } +func TestFormatter_Format_UnicodeDisplayWidthAlignsColumns(t *testing.T) { + f := &Formatter{} + var buf bytes.Buffer + findings := []finding.Finding{ + { + DetectorID: "detector-a", + Severity: finding.SeverityLow, + Redacted: "first-redacted", + SourceMetadata: finding.SourceMetadata{ + FilePath: "設定/秘密.yaml", + }, + }, + { + DetectorID: "detector-b", + Severity: finding.SeverityHigh, + Redacted: "second-redacted", + SourceMetadata: finding.SourceMetadata{ + FilePath: "cafe\u0301/emoji-🔐.yaml", + }, + }, + } + + require.NoError(t, f.Format(&buf, findings)) + lines := strings.Split(buf.String(), "\n") + require.GreaterOrEqual(t, len(lines), 4) + + wantColumn := -1 + for _, item := range []struct { + line int + needle string + }{{0, "REDACTED"}, {2, "first-redacted"}, {3, "second-redacted"}} { + line := lines[item.line] + byteIndex := strings.Index(line, item.needle) + require.NotEqual(t, -1, byteIndex) + column := runewidth.StringWidth(line[:byteIndex]) + if wantColumn < 0 { + wantColumn = column + } + assert.Equal(t, wantColumn, column, "display column drifted for %q", item.needle) + } +} + func TestFormatter_Format_WithRemediation_ShowsRemediationTitle(t *testing.T) { f := &Formatter{} var buf bytes.Buffer diff --git a/internal/remediation/guidance.go b/internal/remediation/guidance.go index c40940a..3b01cc4 100644 --- a/internal/remediation/guidance.go +++ b/internal/remediation/guidance.go @@ -214,6 +214,25 @@ func init() { }, }) + Register("structured-config-secret", finding.Remediation{ + Title: "Move Configuration Secret to Secure Storage", + Steps: []string{ + "Treat the exposed value as compromised and rotate or revoke it at its owning service.", + "Remove the plaintext value from the configuration file and its version history.", + "Store the replacement in a secrets manager or the platform's protected local-secret mechanism.", + "Reference the protected value through environment or secret-manager indirection.", + "Redeploy affected services and verify that the old credential no longer works.", + }, + DocURL: "https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html", + Urgency: "immediate", + Checklist: []string{ + "Review repository, artifact, backup, log, and chat history for additional copies.", + "Audit the owning service for unauthorized use of the exposed credential.", + checkNotifySecurityTeam, + checkScanCodebaseForKey, + }, + }) + Register("openai-api-key", finding.Remediation{ Title: "Rotate OpenAI API Key", Steps: []string{ @@ -505,10 +524,10 @@ func init() { }) Register("twilio-api-key", finding.Remediation{ - Title: "Rotate Twilio API Key", + Title: "Rotate Twilio API Key Pair", Steps: []string{ "Go to Twilio Console > API Keys.", - stepDeleteCompromisedKey, + "Identify the API Key SID paired with the exposed secret and revoke that key.", stepCreateNewAPIKey, }, DocURL: "https://www.twilio.com/docs/iam/api-keys", @@ -715,8 +734,8 @@ func init() { Register("shopify-access-token", finding.Remediation{ Title: "Rotate Shopify Access Token", Steps: []string{ - "Go to Shopify Admin > Apps > Manage private apps.", - "Rotate the compromised access token.", + "Go to Shopify Admin > Settings > Apps and sales channels > Develop apps.", + "Open the affected app and revoke or rotate the compromised Admin API access token.", stepUpdateIntegrationsToken, }, DocURL: "https://shopify.dev/docs/apps/auth", @@ -728,14 +747,14 @@ func init() { }) Register("supabase-service-key", finding.Remediation{ - Title: "Rotate Supabase Service Key", + Title: "Revoke Supabase Personal Access Token", Steps: []string{ - "Go to app.supabase.com > Project Settings > API.", - "Regenerate the service role key.", - "Update all backend services with the new key.", + "Open the Supabase Dashboard account access-token page.", + "Identify and revoke the compromised personal access token.", + "Create a replacement only if the CLI or automation still requires one, then update those consumers.", }, - DocURL: "https://supabase.com/docs/guides/api", - ConsoleURL: "https://app.supabase.com", + DocURL: "https://supabase.com/docs/reference/api/introduction", + ConsoleURL: "https://supabase.com/dashboard/account/tokens", Urgency: "immediate", }) diff --git a/internal/remediation/registry_coverage_test.go b/internal/remediation/registry_coverage_test.go index 4aadfa9..621d47d 100644 --- a/internal/remediation/registry_coverage_test.go +++ b/internal/remediation/registry_coverage_test.go @@ -48,7 +48,7 @@ import ( _ "github.com/HodeTech/leakwatch/internal/detector/figma" // register figma detector _ "github.com/HodeTech/leakwatch/internal/detector/ftp" // register ftp credentials detector _ "github.com/HodeTech/leakwatch/internal/detector/gcp" // register gcp service-account detector - _ "github.com/HodeTech/leakwatch/internal/detector/generic" // register generic api-key detector + _ "github.com/HodeTech/leakwatch/internal/detector/generic" // register generic API-key and structured-config detectors _ "github.com/HodeTech/leakwatch/internal/detector/github" // register github detectors (pat + oauth) _ "github.com/HodeTech/leakwatch/internal/detector/gitlab" // register gitlab detector _ "github.com/HodeTech/leakwatch/internal/detector/grafana" // register grafana detector diff --git a/internal/scanner/scanner.go b/internal/scanner/scanner.go index a0f0d87..a6ba55b 100644 --- a/internal/scanner/scanner.go +++ b/internal/scanner/scanner.go @@ -11,9 +11,11 @@ package scanner import ( "context" + "errors" "fmt" "log/slog" "path/filepath" + "sort" "sync" "time" @@ -59,6 +61,16 @@ type Config struct { VerifyTimeout time.Duration VerifyConcurrency int VerifyRateLimit float64 + // GrafanaInstanceURL is accepted only from the explicit command-line flag, + // never from repository config or detector ExtraData. It is therefore a + // trusted operator choice rather than scan-controlled input. + GrafanaInstanceURL string + // TrustedVerifierOrigins maps a context-required detector ID to an HTTPS + // origin explicitly supplied by the operator on this invocation. The cmd + // layer never resolves this map from project configuration or environment + // variables, preventing scanned repositories from choosing credential + // destinations. + TrustedVerifierOrigins map[string]string // CustomRules are user-defined YAML custom rules from the `custom-rules:` // config block. BuildEngineConfig compiles them into detectors threaded @@ -125,6 +137,11 @@ func BuildEngineConfig(cfg *Config) (engine.Config, error) { slog.Warn("--only-verified has no effect when --no-verify is set") } + verifiers, err := configuredVerifiers(cfg) + if err != nil { + return engine.Config{}, err + } + return engine.Config{ Concurrency: cfg.Concurrency, Detectors: detectors, @@ -132,12 +149,45 @@ func BuildEngineConfig(cfg *Config) (engine.Config, error) { EntropyThreshold: cfg.EntropyThreshold, ShowRaw: cfg.ShowRaw, VerifierConfig: verifierCfg, - Verifiers: verifier.All(), + Verifiers: verifiers, OnlyVerified: cfg.OnlyVerified, MinSeverity: cfg.MinSeverity, }, nil } +func configuredVerifiers(cfg *Config) ([]verifier.Verifier, error) { + verifiers := verifier.All() + origins := make(map[string]string, len(cfg.TrustedVerifierOrigins)+1) + for detectorID, origin := range cfg.TrustedVerifierOrigins { + origins[detectorID] = origin + } + if cfg.GrafanaInstanceURL != "" { + if origin, exists := origins["grafana-api-key"]; exists && origin != cfg.GrafanaInstanceURL { + return nil, fmt.Errorf("configure trusted verification: conflicting Grafana origins were supplied") + } + origins["grafana-api-key"] = cfg.GrafanaInstanceURL + } + if len(origins) == 0 { + return verifiers, nil + } + + ids := make([]string, 0, len(origins)) + for detectorID := range origins { + ids = append(ids, detectorID) + } + sort.Strings(ids) + + configured := verifiers + for _, detectorID := range ids { + var err error + configured, err = verifier.ConfigureTrustedInstance(configured, detectorID, origins[detectorID]) + if err != nil { + return nil, fmt.Errorf("configure trusted verification for %q: %w", detectorID, err) + } + } + return configured, nil +} + // Run executes a single-source scan: it builds the engine, runs the scan under // the given (already signal-aware) context, and post-processes the result // (.leakwatchignore filtering and remediation enrichment). @@ -146,7 +196,9 @@ func BuildEngineConfig(cfg *Config) (engine.Config, error) { // non-nil on interruption (ctx cancelled) or a terminal source failure; in the // interruption case a partial, non-nil result is still returned so the caller can // render partial findings and choose a distinct interrupted exit code. Only a -// pre-scan failure (e.g. source validation) yields a nil result. +// a non-cancellation pre-scan failure yields a nil result. Cancellation during +// source validation returns an interrupted, non-nil result so the CLI can map +// it to exit code 3 instead of a generic failure. func Run(ctx context.Context, cfg *Config, src source.Source) (*engine.ScanResult, error) { engCfg, err := BuildEngineConfig(cfg) if err != nil { @@ -203,6 +255,7 @@ func ScanRepos(ctx context.Context, cfg *Config, repoURLs []string, srcOpts []gi result, scanErr := eng.Scan(ctx, src) if closeErr := src.Close(); closeErr != nil { slog.Warn("failed to clean up repo", "url", displayURL, "error", closeErr) + scanErr = errors.Join(scanErr, fmt.Errorf("failed to clean up repo %s: %w", displayURL, closeErr)) } if result != nil { slog.Info("repository scan completed", "url", displayURL, "findings", len(result.Findings), "files", result.ScannedChunks) @@ -229,7 +282,7 @@ func ScanRepos(ctx context.Context, cfg *Config, repoURLs []string, srcOpts []gi var aggErr error if len(scanErrors) > 0 { - aggErr = fmt.Errorf("%d repository scans failed", len(scanErrors)) + aggErr = fmt.Errorf("%d repository scans failed: %w", len(scanErrors), errors.Join(scanErrors...)) } return combined, aggErr } @@ -273,10 +326,10 @@ func scanReposParallel( mu.Lock() defer mu.Unlock() + if err != nil { + errs = append(errs, err) + } if result == nil { - if err != nil { - errs = append(errs, err) - } return } findings = append(findings, result.Findings...) diff --git a/internal/scanner/scanner_test.go b/internal/scanner/scanner_test.go index 5027866..bb4eb04 100644 --- a/internal/scanner/scanner_test.go +++ b/internal/scanner/scanner_test.go @@ -2,6 +2,7 @@ package scanner import ( "context" + "errors" "fmt" "sync" "testing" @@ -22,9 +23,9 @@ type fakeSource struct { data []byte } -func (f *fakeSource) Type() string { return "fake" } -func (f *fakeSource) Validate() error { return nil } -func (f *fakeSource) Err() error { return nil } +func (f *fakeSource) Type() string { return "fake" } +func (f *fakeSource) Validate(context.Context) error { return nil } +func (f *fakeSource) Err() error { return nil } func (f *fakeSource) Chunks(ctx context.Context) <-chan source.Chunk { ch := make(chan source.Chunk) @@ -179,6 +180,7 @@ func TestScanReposParallel_SharesSingleEngine(t *testing.T) { func TestScanReposParallel_AggregatesFindingsAndErrors(t *testing.T) { eng := engine.New(engine.Config{Concurrency: 1}) + partialErr := fmt.Errorf("partial") scanOne := func(_ context.Context, _ *engine.Engine, url string) (*engine.ScanResult, error) { switch url { @@ -187,8 +189,8 @@ func TestScanReposParallel_AggregatesFindingsAndErrors(t *testing.T) { return nil, fmt.Errorf("clone failed") case "partial": // Partial result WITH an error (e.g. interrupted mid-scan): findings - // are still aggregated, the error is not separately recorded. - return &engine.ScanResult{Findings: []finding.Finding{{ID: "p"}}, ScannedChunks: 1}, fmt.Errorf("partial") + // and its error must both be aggregated. + return &engine.ScanResult{Findings: []finding.Finding{{ID: "p"}}, ScannedChunks: 1}, partialErr default: return &engine.ScanResult{Findings: []finding.Finding{{ID: url}}, ScannedChunks: 2}, nil } @@ -197,7 +199,8 @@ func TestScanReposParallel_AggregatesFindingsAndErrors(t *testing.T) { urls := []string{"ok1", "ok2", "fail", "partial"} findings, chunks, errs := scanReposParallel(context.Background(), eng, urls, 2, scanOne) - assert.Len(t, errs, 1, "only the nil-result failure is recorded as an error") + assert.Len(t, errs, 2, "both fatal and partial-result failures must be recorded") + assert.ErrorIs(t, errors.Join(errs...), partialErr) assert.Len(t, findings, 3, "ok1, ok2 and partial's findings are aggregated") assert.Equal(t, 2+2+1, chunks) } diff --git a/internal/source/container/container.go b/internal/source/container/container.go index 1badf86..8f4dfb8 100644 --- a/internal/source/container/container.go +++ b/internal/source/container/container.go @@ -122,7 +122,10 @@ func (s *ContainerSource) captureErr(err error) { } // Validate checks that the image reference is parseable and accessible. -func (s *ContainerSource) Validate() error { +func (s *ContainerSource) Validate(ctx context.Context) error { + if err := ctx.Err(); err != nil { + return err + } _, err := name.ParseReference(s.imageRef) if err != nil { return fmt.Errorf("invalid image reference %q: %w", s.imageRef, err) diff --git a/internal/source/container/container_test.go b/internal/source/container/container_test.go index dc8a8a1..5591413 100644 --- a/internal/source/container/container_test.go +++ b/internal/source/container/container_test.go @@ -19,17 +19,17 @@ func TestContainerSource_Type_ReturnsContainer(t *testing.T) { func TestContainerSource_Validate_ValidRef_ReturnsNoError(t *testing.T) { s := New("nginx:latest") - assert.NoError(t, s.Validate()) + assert.NoError(t, s.Validate(context.Background())) } func TestContainerSource_Validate_InvalidRef_ReturnsError(t *testing.T) { s := New(":::invalid") - assert.Error(t, s.Validate()) + assert.Error(t, s.Validate(context.Background())) } func TestContainerSource_Validate_FullRef_ReturnsNoError(t *testing.T) { s := New("ghcr.io/org/repo:v1.0.0") - assert.NoError(t, s.Validate()) + assert.NoError(t, s.Validate(context.Background())) } func TestShouldSkipContainerPath(t *testing.T) { diff --git a/internal/source/filesystem/filesystem.go b/internal/source/filesystem/filesystem.go index 4efb2b5..913f812 100644 --- a/internal/source/filesystem/filesystem.go +++ b/internal/source/filesystem/filesystem.go @@ -87,11 +87,14 @@ func (s *FilesystemSource) Type() string { // Validate checks that every scan root exists and is accessible. A root may be // either a directory or a regular file; only inaccessibility (e.g. a // non-existent path or a permission error) is rejected. -func (s *FilesystemSource) Validate() error { +func (s *FilesystemSource) Validate(ctx context.Context) error { if len(s.roots) == 0 { return fmt.Errorf("no scan paths provided") } for _, root := range s.roots { + if err := ctx.Err(); err != nil { + return err + } if _, err := os.Stat(root); err != nil { return fmt.Errorf("source path inaccessible %s: %w", root, err) } diff --git a/internal/source/filesystem/filesystem_test.go b/internal/source/filesystem/filesystem_test.go index 4a3e33f..92d4d16 100644 --- a/internal/source/filesystem/filesystem_test.go +++ b/internal/source/filesystem/filesystem_test.go @@ -56,18 +56,18 @@ func TestFilesystemSource_New_CleansPath(t *testing.T) { } } -// Validate() calls os.Stat directly against s.root, so these tests genuinely +// Validate(ctx) calls os.Stat directly against s.root, so these tests genuinely // need a real filesystem rather than an injectable fs.FS. func TestFilesystemSource_Validate_ValidDir(t *testing.T) { dir := t.TempDir() s := New(dir) - assert.NoError(t, s.Validate()) + assert.NoError(t, s.Validate(context.Background())) } func TestFilesystemSource_Validate_NonExistentDir(t *testing.T) { s := New("/nonexistent/path") - assert.Error(t, s.Validate()) + assert.Error(t, s.Validate(context.Background())) } func TestFilesystemSource_Validate_SingleFile(t *testing.T) { @@ -78,7 +78,7 @@ func TestFilesystemSource_Validate_SingleFile(t *testing.T) { // A single file is now a valid scan root: `scan fs ` must work. s := New(f.Name()) - assert.NoError(t, s.Validate()) + assert.NoError(t, s.Validate(context.Background())) } func TestFilesystemSource_Validate_MultipleRoots(t *testing.T) { @@ -87,10 +87,10 @@ func TestFilesystemSource_Validate_MultipleRoots(t *testing.T) { require.NoError(t, os.WriteFile(f, []byte("x"), 0o644)) s := NewMulti([]string{dir, f}) - assert.NoError(t, s.Validate()) + assert.NoError(t, s.Validate(context.Background())) bad := NewMulti([]string{dir, "/nonexistent/path"}) - assert.Error(t, bad.Validate()) + assert.Error(t, bad.Validate(context.Background())) } // shouldSkip is pure filtering logic over a path string and an fs.DirEntry; diff --git a/internal/source/gcs/gcs.go b/internal/source/gcs/gcs.go index 0e3df71..158cd17 100644 --- a/internal/source/gcs/gcs.go +++ b/internal/source/gcs/gcs.go @@ -10,6 +10,7 @@ import ( "fmt" "io" "log/slog" + "sync" "time" gcsstorage "cloud.google.com/go/storage" @@ -24,11 +25,8 @@ import ( // defaultMaxFileSize is the maximum object size to scan (10 MB). const defaultMaxFileSize int64 = 10 * 1024 * 1024 -// validateTimeout bounds the network calls made by Validate. The -// source.Source interface's Validate() method takes no context.Context -// parameter, so the caller's own cancellation cannot be threaded through -// here; a bounded timeout at least prevents an unreachable/misconfigured -// bucket from hanging Validate indefinitely. +// validateTimeout bounds validation when the caller provides no earlier +// deadline. An earlier caller deadline or cancellation always wins. const validateTimeout = 30 * time.Second // gcsClient defines the subset of the GCS API used by GCSSource. @@ -102,6 +100,7 @@ type GCSSource struct { maxFileSize int64 bufferSize int excludePaths []string + clientMu sync.RWMutex client gcsClient // err records the first terminal failure that aborted listing (client @@ -138,6 +137,21 @@ func (s *GCSSource) Err() error { return s.err } +// Close releases the GCS client. A close failure retains the client so callers +// can report the error and retry cleanup; successful repeated calls are no-ops. +func (s *GCSSource) Close() error { + s.clientMu.Lock() + defer s.clientMu.Unlock() + if s.client == nil { + return nil + } + if err := s.client.Close(); err != nil { + return fmt.Errorf("gcs client close failed: %w", err) + } + s.client = nil + return nil +} + // captureErr records the first terminal error that aborted chunk production. It // is called only from the single Chunks goroutine, before close(ch), so a plain // field write is safe (the channel close/drain publishes it to Err's reader). @@ -156,19 +170,24 @@ func (s *GCSSource) captureErr(err error) { // Validate checks that the GCS bucket is accessible. // It initializes the GCS client if not already set and checks bucket attributes. -func (s *GCSSource) Validate() error { +func (s *GCSSource) Validate(ctx context.Context) error { if s.bucket == "" { return fmt.Errorf("gcs bucket name is required") } - ctx, cancel := context.WithTimeout(context.Background(), validateTimeout) + ctx, cancel := context.WithTimeout(ctx, validateTimeout) defer cancel() if err := s.ensureClient(ctx); err != nil { return fmt.Errorf("gcs client initialization failed: %w", err) } - _, err := s.client.Bucket(s.bucket).Attrs(ctx) + client, release := s.acquireClient() + if client == nil { + return fmt.Errorf("gcs client was closed before validation") + } + defer release() + _, err := client.Bucket(s.bucket).Attrs(ctx) if err != nil { return fmt.Errorf("gcs bucket inaccessible %q: %w", s.bucket, err) } @@ -188,19 +207,22 @@ func (s *GCSSource) Chunks(ctx context.Context) <-chan source.Chunk { s.captureErr(fmt.Errorf("gcs client initialization failed: %w", err)) return } - defer func() { - if err := s.client.Close(); err != nil { - slog.Warn("gcs client close failed", "error", err) - } - }() + client, release := s.acquireClient() + if client == nil { + s.captureErr(fmt.Errorf("gcs client was closed before object listing")) + return + } + defer release() - s.listAndSendChunks(ctx, ch) + s.listAndSendChunks(ctx, ch, client) }() return ch } // ensureClient initializes the GCS client if not already set. func (s *GCSSource) ensureClient(ctx context.Context) error { + s.clientMu.Lock() + defer s.clientMu.Unlock() if s.client != nil { return nil } @@ -219,14 +241,23 @@ func (s *GCSSource) ensureClient(ctx context.Context) error { return nil } +func (s *GCSSource) acquireClient() (gcsClient, func()) { + s.clientMu.RLock() + if s.client == nil { + s.clientMu.RUnlock() + return nil, func() {} + } + return s.client, s.clientMu.RUnlock +} + // listAndSendChunks iterates through bucket objects and emits chunks. -func (s *GCSSource) listAndSendChunks(ctx context.Context, ch chan<- source.Chunk) { +func (s *GCSSource) listAndSendChunks(ctx context.Context, ch chan<- source.Chunk, client gcsClient) { query := &gcsstorage.Query{} if s.prefix != "" { query.Prefix = s.prefix } - it := s.client.Bucket(s.bucket).Objects(ctx, query) + it := client.Bucket(s.bucket).Objects(ctx, query) for { select { case <-ctx.Done(): @@ -269,7 +300,7 @@ func (s *GCSSource) listAndSendChunks(ctx context.Context, ch chan<- source.Chun continue } - data, err := s.downloadObject(ctx, key) + data, err := s.downloadObject(ctx, client, key) if err != nil { slog.Warn("gcs object download failed", "key", key, "error", err) continue @@ -302,8 +333,8 @@ func (s *GCSSource) listAndSendChunks(ctx context.Context, ch chan<- source.Chun } // downloadObject fetches the content of a single GCS object. -func (s *GCSSource) downloadObject(ctx context.Context, key string) ([]byte, error) { - reader, err := s.client.Bucket(s.bucket).Object(key).NewReader(ctx) +func (s *GCSSource) downloadObject(ctx context.Context, client gcsClient, key string) ([]byte, error) { + reader, err := client.Bucket(s.bucket).Object(key).NewReader(ctx) if err != nil { return nil, fmt.Errorf("open object %q: %w", key, err) } diff --git a/internal/source/gcs/gcs_test.go b/internal/source/gcs/gcs_test.go index bb1acd5..a71fa04 100644 --- a/internal/source/gcs/gcs_test.go +++ b/internal/source/gcs/gcs_test.go @@ -6,6 +6,7 @@ import ( "io" "strings" "testing" + "time" gcsstorage "cloud.google.com/go/storage" "github.com/stretchr/testify/assert" @@ -17,8 +18,10 @@ import ( // mockGCSClient implements the gcsClient interface for testing. type mockGCSClient struct { - buckets map[string]*mockBucketHandle - closed bool + buckets map[string]*mockBucketHandle + closed bool + closeErr error + closeCalls int } func (m *mockGCSClient) Bucket(name string) bucketHandle { @@ -29,19 +32,32 @@ func (m *mockGCSClient) Bucket(name string) bucketHandle { } func (m *mockGCSClient) Close() error { + m.closeCalls++ + if m.closeErr != nil { + return m.closeErr + } m.closed = true return nil } // mockBucketHandle implements the bucketHandle interface for testing. type mockBucketHandle struct { - name string - notFound bool - objects []*gcsstorage.ObjectAttrs - data map[string]string + name string + notFound bool + objects []*gcsstorage.ObjectAttrs + data map[string]string + attrsBlock bool + attrsDeadline time.Time + attrsHasDeadline bool + readerStarted chan struct{} } -func (b *mockBucketHandle) Attrs(_ context.Context) (*gcsstorage.BucketAttrs, error) { +func (b *mockBucketHandle) Attrs(ctx context.Context) (*gcsstorage.BucketAttrs, error) { + b.attrsDeadline, b.attrsHasDeadline = ctx.Deadline() + if b.attrsBlock { + <-ctx.Done() + return nil, ctx.Err() + } if b.notFound { return nil, fmt.Errorf("bucket not found") } @@ -64,15 +80,22 @@ func (b *mockBucketHandle) Object(name string) objectHandle { if b.data != nil { content = b.data[name] } - return &mockObjectHandle{content: content} + return &mockObjectHandle{content: content, readerStarted: b.readerStarted} } // mockObjectHandle implements the objectHandle interface for testing. type mockObjectHandle struct { - content string + content string + readerStarted chan struct{} } func (o *mockObjectHandle) NewReader(_ context.Context) (io.ReadCloser, error) { + if o.readerStarted != nil { + select { + case o.readerStarted <- struct{}{}: + default: + } + } return io.NopCloser(strings.NewReader(o.content)), nil } @@ -136,7 +159,7 @@ func TestWithMaxFileSize_ValidValue_Applied(t *testing.T) { func TestGCSSource_Validate_EmptyBucket_ReturnsError(t *testing.T) { s := New("") - err := s.Validate() + err := s.Validate(context.Background()) require.Error(t, err) assert.Contains(t, err.Error(), "bucket name is required") } @@ -149,7 +172,34 @@ func TestGCSSource_Validate_AccessibleBucket_ReturnsNil(t *testing.T) { } s := New("my-bucket") s.client = mock - assert.NoError(t, s.Validate()) + assert.NoError(t, s.Validate(context.Background())) +} + +func TestGCSSource_Validate_AppliesDefaultDeadline(t *testing.T) { + bucket := &mockBucketHandle{name: "my-bucket"} + mock := &mockGCSClient{buckets: map[string]*mockBucketHandle{"my-bucket": bucket}} + s := New("my-bucket") + s.client = mock + started := time.Now() + + require.NoError(t, s.Validate(context.Background())) + require.True(t, bucket.attrsHasDeadline) + assert.WithinDuration(t, started.Add(validateTimeout), bucket.attrsDeadline, time.Second) +} + +func TestGCSSource_Validate_CallerDeadlineCancelsRequest(t *testing.T) { + bucket := &mockBucketHandle{name: "my-bucket", attrsBlock: true} + mock := &mockGCSClient{buckets: map[string]*mockBucketHandle{"my-bucket": bucket}} + s := New("my-bucket") + s.client = mock + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond) + defer cancel() + started := time.Now() + + err := s.Validate(ctx) + require.ErrorIs(t, err, context.DeadlineExceeded) + assert.Less(t, time.Since(started), time.Second) + assert.WithinDuration(t, started.Add(20*time.Millisecond), bucket.attrsDeadline, 50*time.Millisecond) } func TestGCSSource_Validate_InaccessibleBucket_ReturnsError(t *testing.T) { @@ -158,9 +208,11 @@ func TestGCSSource_Validate_InaccessibleBucket_ReturnsError(t *testing.T) { } s := New("missing-bucket") s.client = mock - err := s.Validate() + err := s.Validate(context.Background()) require.Error(t, err) assert.Contains(t, err.Error(), "inaccessible") + require.NoError(t, s.Close()) + assert.True(t, mock.closed, "a client created before validation failure must remain closeable") } func TestGCSSource_Chunks_SendsTextObjects(t *testing.T) { @@ -392,7 +444,7 @@ func TestGCSSource_DownloadObject_AtLimit_NotSkipped(t *testing.T) { s := New("my-bucket", WithMaxFileSize(1024)) s.client = mock - data, err := s.downloadObject(context.Background(), "exact.txt") + data, err := s.downloadObject(context.Background(), mock, "exact.txt") require.NoError(t, err) assert.Len(t, data, 1024) } @@ -434,7 +486,7 @@ func TestGCSSource_New_WithExcludePaths_StoresPatterns(t *testing.T) { assert.Equal(t, []string{"a/**", "b"}, s.excludePaths) } -func TestGCSSource_Chunks_ClosesClientOnCompletion(t *testing.T) { +func TestGCSSource_CallerOwnsClientCloseAfterChunksComplete(t *testing.T) { mock := &mockGCSClient{ buckets: map[string]*mockBucketHandle{ "my-bucket": { @@ -451,10 +503,70 @@ func TestGCSSource_Chunks_ClosesClientOnCompletion(t *testing.T) { s.client = mock for range s.Chunks(context.Background()) { - // Drain the channel so Chunks' goroutine can finish and close the client. + // Drain the channel so Chunks' goroutine releases its client lease. + } + + assert.False(t, mock.closed, "chunk production must not compete with the CLI cleanup owner") + require.NoError(t, s.Close()) + assert.True(t, mock.closed) + assert.Nil(t, s.client, "successful close must make later cleanup idempotent") +} + +func TestGCSSource_CloseWaitsForCancelledProducerAndClosesOnce(t *testing.T) { + readerStarted := make(chan struct{}, 1) + mock := &mockGCSClient{buckets: map[string]*mockBucketHandle{ + "my-bucket": { + name: "my-bucket", + objects: []*gcsstorage.ObjectAttrs{{Name: "secret.txt", Size: 6}}, + data: map[string]string{"secret.txt": "secret"}, + readerStarted: readerStarted, + }, + }} + s := New("my-bucket") + s.bufferSize = 0 + s.client = mock + ctx, cancel := context.WithCancel(context.Background()) + chunks := s.Chunks(ctx) + + select { + case <-readerStarted: + case <-time.After(time.Second): + t.Fatal("producer did not begin object download") + } + closed := make(chan error, 1) + go func() { closed <- s.Close() }() + select { + case err := <-closed: + t.Fatalf("Close returned while producer still held the client lease: %v", err) + case <-time.After(20 * time.Millisecond): } - assert.True(t, mock.closed, "gcs client must be closed once Chunks completes") + cancel() + for range chunks { + } + require.NoError(t, <-closed) + assert.Equal(t, 1, mock.closeCalls) + require.NoError(t, s.Close()) + assert.Equal(t, 1, mock.closeCalls) +} + +func TestGCSSource_Close_FailureRemainsRetryable(t *testing.T) { + closeErr := fmt.Errorf("synthetic close failure") + mock := &mockGCSClient{buckets: map[string]*mockBucketHandle{}, closeErr: closeErr} + s := New("my-bucket") + s.client = mock + + err := s.Close() + require.ErrorIs(t, err, closeErr) + assert.Same(t, mock, s.client) + assert.Equal(t, 1, mock.closeCalls) + + mock.closeErr = nil + require.NoError(t, s.Close()) + assert.Nil(t, s.client) + assert.Equal(t, 2, mock.closeCalls) + require.NoError(t, s.Close()) + assert.Equal(t, 2, mock.closeCalls, "an already-closed source must be a no-op") } func TestGCSSource_Chunks_WithPrefix_FiltersObjects(t *testing.T) { diff --git a/internal/source/git/git.go b/internal/source/git/git.go index 3c50628..1e15ca8 100644 --- a/internal/source/git/git.go +++ b/internal/source/git/git.go @@ -53,6 +53,7 @@ type GitSource struct { excludePaths []string tmpDir string // Temporary directory for cloned repos resolvedBranch string // Cached branch resolution + removeAll func(string) error // err records the first terminal failure that aborted history production // (start-commit resolution, git log, or the history walk). It is written @@ -87,23 +88,37 @@ func (s *GitSource) Type() string { // when --since-commit is set it verifies the given commit is an ancestor of the // walk's starting point; otherwise the diff-based walk would silently fall back // to scanning the entire history or an unintended branch. -func (s *GitSource) Validate() error { - if s.isRemote() { - if err := s.cloneRemote(); err != nil { + +func (s *GitSource) Validate(ctx context.Context) error { + if err := ctx.Err(); err != nil { + return err + } + remote, cloneTarget, err := classifyGitTarget(s.target) + if err != nil { + return fmt.Errorf("invalid git target %s: %w", s.displayTarget, err) + } + if remote { + if err := s.cloneRemote(ctx, cloneTarget); err != nil { return err } } else if err := s.openLocal(); err != nil { return err } + if err := ctx.Err(); err != nil { + return err + } if s.branch != "" { + if err := ctx.Err(); err != nil { + return err + } if _, err := s.resolveStartHash(); err != nil { return err } } if s.sinceCommit != "" { - if err := s.validateSinceCommit(); err != nil { + if err := s.validateSinceCommit(ctx); err != nil { return err } } @@ -115,27 +130,36 @@ func (s *GitSource) Validate() error { // ancestor of the walk's starting commit (the configured branch tip, or HEAD). // Returning an explicit error prevents the diff-based scan from silently // degrading into a full-history scan. -func (s *GitSource) validateSinceCommit() error { - sinceCommitObj, err := s.resolveCommitHash(s.sinceCommit) +func (s *GitSource) validateSinceCommit(ctx context.Context) error { + sinceCommitObj, err := s.resolveCommitHash(ctx, s.sinceCommit) if err != nil { return err } + if err := ctx.Err(); err != nil { + return err + } startHash, err := s.resolveStartHash() if err != nil { return fmt.Errorf("failed to resolve start commit for since-commit check: %w", err) } + if err := ctx.Err(); err != nil { + return err + } startCommit, err := s.repo.CommitObject(startHash) if err != nil { return fmt.Errorf("failed to resolve start commit for since-commit check: %w", err) } + if err := ctx.Err(); err != nil { + return err + } if sinceCommitObj.Hash == startCommit.Hash { return nil } - isAncestor, err := sinceCommitObj.IsAncestor(startCommit) + isAncestor, err := isAncestorContext(ctx, sinceCommitObj, startCommit) if err != nil { return fmt.Errorf("failed to check ancestry of since-commit %q: %w", s.sinceCommit, err) } @@ -151,27 +175,43 @@ func (s *GitSource) validateSinceCommit() error { // resolved by scanning the object store for a unique match, so the documented // `--since-commit abc1234` short-hash usage works rather than silently producing // a wrong zero-padded hash via plumbing.NewHash. -func (s *GitSource) resolveCommitHash(ref string) (*object.Commit, error) { - ref = strings.TrimSpace(ref) - - if len(ref) == fullHashLen { - if !isHexString(ref) { - return nil, fmt.Errorf("since-commit %q is not a valid commit hash", ref) - } - c, err := s.repo.CommitObject(plumbing.NewHash(ref)) - if err != nil { - return nil, fmt.Errorf("since-commit %q not found: %w", ref, err) - } - return c, nil +func (s *GitSource) resolveCommitHash(ctx context.Context, ref string) (*object.Commit, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + normalized, full, err := normalizeCommitHashReference(ref) + if err != nil { + return nil, err } + if full { + return s.resolveFullCommitHash(ctx, normalized) + } + return s.resolveAbbreviatedCommitHash(ctx, normalized) +} +func normalizeCommitHashReference(ref string) (string, bool, error) { + ref = strings.TrimSpace(ref) if len(ref) < minAbbrevHashLen { - return nil, fmt.Errorf("since-commit %q is too short: provide at least %d hex characters or a full 40-character SHA", ref, minAbbrevHashLen) + return "", false, fmt.Errorf("since-commit %q is too short: provide at least %d hex characters or a full 40-character SHA", ref, minAbbrevHashLen) } if len(ref) > fullHashLen || !isHexString(ref) { - return nil, fmt.Errorf("since-commit %q is not a valid commit hash", ref) + return "", false, fmt.Errorf("since-commit %q is not a valid commit hash", ref) + } + return ref, len(ref) == fullHashLen, nil +} + +func (s *GitSource) resolveFullCommitHash(ctx context.Context, ref string) (*object.Commit, error) { + commit, err := s.repo.CommitObject(plumbing.NewHash(ref)) + if err != nil { + return nil, fmt.Errorf("since-commit %q not found: %w", ref, err) } + if err := ctx.Err(); err != nil { + return nil, err + } + return commit, nil +} +func (s *GitSource) resolveAbbreviatedCommitHash(ctx context.Context, ref string) (*object.Commit, error) { // Deliberately NOT go-git's Repository.ResolveRevision: it resolves a hash // prefix by collecting every candidate and returning the first that resolves, // with no ambiguity signal (there is no ErrAmbiguousRevision in go-git). For a @@ -191,6 +231,9 @@ func (s *GitSource) resolveCommitHash(ref string) (*object.Commit, error) { ambiguous bool ) err = iter.ForEach(func(c *object.Commit) error { + if err := ctx.Err(); err != nil { + return err + } if !strings.HasPrefix(c.Hash.String(), lower) { return nil } @@ -213,6 +256,25 @@ func (s *GitSource) resolveCommitHash(ref string) (*object.Commit, error) { return match, nil } +func isAncestorContext(ctx context.Context, ancestor, descendant *object.Commit) (bool, error) { + iter := object.NewCommitPreorderIter(descendant, nil, nil) + defer iter.Close() + for { + if err := ctx.Err(); err != nil { + return false, err + } + commit, err := iter.Next() + switch { + case errors.Is(err, io.EOF): + return false, nil + case err != nil: + return false, err + case commit.Hash == ancestor.Hash: + return true, nil + } + } +} + // resolveStartHash returns the commit hash the history walk should start from. // When --branch is configured it resolves that branch's local ref (so the flag // steers which commits are scanned, not just the metadata label); otherwise it @@ -236,7 +298,11 @@ func (s *GitSource) resolveStartHash() (plumbing.Hash, error) { // the temporary directory. func (s *GitSource) Close() error { if s.tmpDir != "" { - if err := os.RemoveAll(s.tmpDir); err != nil { + removeAll := s.removeAll + if removeAll == nil { + removeAll = os.RemoveAll + } + if err := removeAll(s.tmpDir); err != nil { return fmt.Errorf("failed to remove temp directory %s: %w", s.tmpDir, err) } s.tmpDir = "" @@ -245,31 +311,75 @@ func (s *GitSource) Close() error { } func (s *GitSource) isRemote() bool { - return strings.HasPrefix(s.target, "http://") || - strings.HasPrefix(s.target, "https://") || - strings.HasPrefix(s.target, "git@") || - strings.HasPrefix(s.target, "ssh://") + remote, _, err := classifyGitTarget(s.target) + return err == nil && remote +} + +// classifyGitTarget distinguishes the supported network transports from local +// paths without relying on case-sensitive string prefixes. URI schemes are +// case-insensitive under RFC 3986; unsupported schemes fail closed instead of +// being reinterpreted as local filesystem paths. Windows drive-letter paths +// remain local even when this code is cross-compiled on another platform. +func classifyGitTarget(target string) (remote bool, normalized string, err error) { + if strings.HasPrefix(target, "git@") { + return true, target, nil + } + if len(target) >= 3 && target[1] == ':' && + ((target[0] >= 'a' && target[0] <= 'z') || (target[0] >= 'A' && target[0] <= 'Z')) && + (target[2] == '/' || target[2] == '\\') { + return false, target, nil + } + + // A colon is legal in a local Unix filename. Treat a target as a URI only + // when it uses the explicit scheme:// form; SCP syntax was handled above. + if !strings.Contains(target, "://") { + return false, target, nil + } + parsed, parseErr := url.Parse(target) + if parseErr != nil { + return false, "", fmt.Errorf("malformed target") + } + if parsed.Scheme == "" { + return false, target, nil + } + switch strings.ToLower(parsed.Scheme) { + case "http", "https", "ssh": + parsed.Scheme = strings.ToLower(parsed.Scheme) + return true, parsed.String(), nil + default: + return false, "", fmt.Errorf("unsupported URL scheme %q", parsed.Scheme) + } } func (s *GitSource) openLocal() error { + info, err := os.Stat(s.target) + if err != nil { + return fmt.Errorf("failed to inspect git target %s: %w", s.displayTarget, err) + } + if !info.IsDir() { + return fmt.Errorf("git target %s is not a directory", s.displayTarget) + } // DetectDotGit walks up parent directories to find the repository root, // matching real git tooling when pointed at a subdirectory of a repo. repo, err := git.PlainOpenWithOptions(s.target, &git.PlainOpenOptions{DetectDotGit: true}) if err != nil { - return fmt.Errorf("failed to open git repository %s: %w", s.target, err) + return fmt.Errorf("failed to open git repository %s: %w", s.displayTarget, err) } s.repo = repo return nil } -func (s *GitSource) cloneRemote() error { +func (s *GitSource) cloneRemote(ctx context.Context, cloneTarget string) error { tmpDir, err := os.MkdirTemp("", "leakwatch-clone-*") if err != nil { return fmt.Errorf("failed to create temp directory: %w", err) } + // Own the directory immediately so every failure path can clean it through + // Close and a failed cleanup remains retryable by the caller. + s.tmpDir = tmpDir cloneOpts := &git.CloneOptions{ - URL: s.target, + URL: cloneTarget, Progress: nil, } @@ -284,19 +394,28 @@ func (s *GitSource) cloneRemote() error { slog.Info("cloning remote repository", "url", s.displayTarget, "tmpDir", tmpDir) - repo, err := git.PlainClone(tmpDir, false, cloneOpts) + repo, err := git.PlainCloneContext(ctx, tmpDir, false, cloneOpts) if err != nil { - _ = os.RemoveAll(tmpDir) // go-git's HTTP transport and endpoint layers re-embed the raw // user:password@host userinfo from the clone URL into their error // strings, so the raw err must never be wrapped with %w. Sanitize it // first so no credential can reach errors.Unwrap chains, stderr, or CI // logs. - return fmt.Errorf("failed to clone git repository %s: %w", - s.displayTarget, sanitizeCloneError(err, s.target, s.displayTarget)) + var cloneErr error + if ctxErr := ctx.Err(); ctxErr != nil { + // Preserve cancellation identity so the CLI maps an interrupted clone + // to exit 3. The context error itself contains no clone credential. + cloneErr = ctxErr + } else { + cloneErr = fmt.Errorf("failed to clone git repository %s: %w", + s.displayTarget, sanitizeCloneError(err, s.target, s.displayTarget)) + } + if cleanupErr := s.Close(); cleanupErr != nil { + return errors.Join(cloneErr, cleanupErr) + } + return cloneErr } s.repo = repo - s.tmpDir = tmpDir return nil } @@ -416,7 +535,7 @@ func (s *GitSource) chunksFullHistory(ctx context.Context, ch chan<- source.Chun // (exclusive) and the branch tip / HEAD (inclusive), diffing each commit against // its parent. func (s *GitSource) chunksSinceCommit(ctx context.Context, ch chan<- source.Chunk) { - sinceCommitObj, err := s.resolveCommitHash(s.sinceCommit) + sinceCommitObj, err := s.resolveCommitHash(ctx, s.sinceCommit) if err != nil { slog.Error("since-commit resolution failed", "commit", s.sinceCommit, "error", err) s.captureErr(fmt.Errorf("since-commit resolution failed: %w", err)) diff --git a/internal/source/git/git_test.go b/internal/source/git/git_test.go index 01c190e..9430f3e 100644 --- a/internal/source/git/git_test.go +++ b/internal/source/git/git_test.go @@ -2,7 +2,10 @@ package git import ( "context" + "errors" "fmt" + "net/http" + "net/http/httptest" "os" "path/filepath" "strings" @@ -16,6 +19,19 @@ import ( "github.com/stretchr/testify/require" ) +type cancelAfterErrChecks struct { + context.Context + remaining int +} + +func (c *cancelAfterErrChecks) Err() error { + c.remaining-- + if c.remaining <= 0 { + return context.Canceled + } + return nil +} + // initTestRepo creates a temporary git repository for testing. func initTestRepo(t *testing.T, files map[string]string) (string, *gogit.Repository) { t.Helper() @@ -81,18 +97,18 @@ func TestGitSource_Validate_ValidRepo(t *testing.T) { dir, _ := initTestRepo(t, map[string]string{"README.md": "hello"}) s := New(dir) - assert.NoError(t, s.Validate()) + assert.NoError(t, s.Validate(context.Background())) } func TestGitSource_Validate_NonExistentRepo(t *testing.T) { s := New("/nonexistent/repo") - assert.Error(t, s.Validate()) + assert.Error(t, s.Validate(context.Background())) } func TestGitSource_Validate_NotAGitRepo(t *testing.T) { dir := t.TempDir() s := New(dir) - assert.Error(t, s.Validate()) + assert.Error(t, s.Validate(context.Background())) } func TestGitSource_Chunks_ReadsCommitHistory(t *testing.T) { @@ -102,7 +118,7 @@ func TestGitSource_Chunks_ReadsCommitHistory(t *testing.T) { }) s := New(dir) - require.NoError(t, s.Validate()) + require.NoError(t, s.Validate(context.Background())) ctx := context.Background() var chunks []string @@ -121,7 +137,7 @@ func TestGitSource_Chunks_IncludesCommitMetadata(t *testing.T) { }) s := New(dir) - require.NoError(t, s.Validate()) + require.NoError(t, s.Validate(context.Background())) ctx := context.Background() for chunk := range s.Chunks(ctx) { @@ -149,7 +165,7 @@ func TestGitSource_Chunks_MultipleCommits(t *testing.T) { }, "third commit") s := New(dir) - require.NoError(t, s.Validate()) + require.NoError(t, s.Validate(context.Background())) ctx := context.Background() seen := make(map[string]bool) @@ -183,7 +199,7 @@ func TestGitSource_Chunks_SinceCommit(t *testing.T) { }, "new commit 2") s := New(dir, WithSinceCommit(sinceHash)) - require.NoError(t, s.Validate()) + require.NoError(t, s.Validate(context.Background())) ctx := context.Background() var files []string @@ -202,7 +218,7 @@ func TestGitSource_Validate_SinceCommitNotFound_ReturnsError(t *testing.T) { // A well-formed but non-existent commit hash. s := New(dir, WithSinceCommit("0123456789abcdef0123456789abcdef01234567")) - err := s.Validate() + err := s.Validate(context.Background()) require.Error(t, err) assert.Contains(t, err.Error(), "not found") } @@ -232,7 +248,7 @@ func TestGitSource_Validate_SinceCommitNotAncestor_ReturnsError(t *testing.T) { // sideHash lives only on sidebranch, so it is not an ancestor of HEAD (main). s := New(dir, WithSinceCommit(sideHash)) - err = s.Validate() + err = s.Validate(context.Background()) require.Error(t, err) assert.Contains(t, err.Error(), "not an ancestor") } @@ -247,7 +263,7 @@ func TestGitSource_Validate_SinceCommitIsAncestor_ReturnsNil(t *testing.T) { addCommit(t, dir, repo, map[string]string{"b.txt": "more"}, "second commit") s := New(dir, WithSinceCommit(baseHash)) - assert.NoError(t, s.Validate()) + assert.NoError(t, s.Validate(context.Background())) } func TestGitSource_Validate_SinceCommitEqualsHead_ReturnsNil(t *testing.T) { @@ -258,7 +274,7 @@ func TestGitSource_Validate_SinceCommitEqualsHead_ReturnsNil(t *testing.T) { headHash := headRef.Hash().String() s := New(dir, WithSinceCommit(headHash)) - assert.NoError(t, s.Validate()) + assert.NoError(t, s.Validate(context.Background())) } func TestGitSource_Chunks_WithExcludePaths_SkipsMatching(t *testing.T) { @@ -269,7 +285,7 @@ func TestGitSource_Chunks_WithExcludePaths_SkipsMatching(t *testing.T) { }) s := New(dir, WithExcludePaths([]string{"vendor/**", "node_modules/**"})) - require.NoError(t, s.Validate()) + require.NoError(t, s.Validate(context.Background())) ctx := context.Background() var files []string @@ -321,7 +337,7 @@ func TestGitSource_Chunks_WithSince(t *testing.T) { require.NoError(t, err) s := New(dir, WithSince(cutoff)) - require.NoError(t, s.Validate()) + require.NoError(t, s.Validate(context.Background())) ctx := context.Background() var files []string @@ -336,7 +352,7 @@ func TestGitSource_Err_SuccessfulChunks_ReturnsNil(t *testing.T) { dir, _ := initTestRepo(t, map[string]string{"README.md": "hello"}) s := New(dir) - require.NoError(t, s.Validate()) + require.NoError(t, s.Validate(context.Background())) for range s.Chunks(context.Background()) { } @@ -346,7 +362,7 @@ func TestGitSource_Err_SuccessfulChunks_ReturnsNil(t *testing.T) { func TestGitSource_Err_HistoryWalkFailure_ReturnsError(t *testing.T) { // A freshly-initialized repository has no commits, so HEAD does not resolve. - // Validate() succeeds (the repo opens), but the history walk in Chunks fails + // Validate(ctx) succeeds (the repo opens), but the history walk in Chunks fails // to resolve a start commit — a terminal error that must be reported via Err // rather than silently yielding a clean, empty scan. dir := t.TempDir() @@ -354,7 +370,7 @@ func TestGitSource_Err_HistoryWalkFailure_ReturnsError(t *testing.T) { require.NoError(t, err) s := New(dir) - require.NoError(t, s.Validate()) + require.NoError(t, s.Validate(context.Background())) var count int for range s.Chunks(context.Background()) { @@ -397,7 +413,7 @@ func TestGitSource_Chunks_ContextCancellation(t *testing.T) { }) s := New(dir) - require.NoError(t, s.Validate()) + require.NoError(t, s.Validate(context.Background())) ctx, cancel := context.WithCancel(context.Background()) cancel() // Cancel immediately. @@ -422,7 +438,7 @@ func TestGitSource_Chunks_SkipsLargeFiles(t *testing.T) { }) s := New(dir, WithMaxFileSize(512)) - require.NoError(t, s.Validate()) + require.NoError(t, s.Validate(context.Background())) ctx := context.Background() var files []string @@ -440,7 +456,9 @@ func TestGitSource_IsRemote(t *testing.T) { expected bool }{ {"https://github.com/org/repo.git", true}, + {"HTTPS://github.com/org/repo.git", true}, {"http://github.com/org/repo.git", true}, + {"SSH://git@github.com/org/repo.git", true}, {"git@github.com:org/repo.git", true}, {"ssh://git@github.com/org/repo.git", true}, {"/local/path/to/repo", false}, @@ -455,6 +473,53 @@ func TestGitSource_IsRemote(t *testing.T) { } } +func TestClassifyGitTarget_RejectsUnsupportedScheme(t *testing.T) { + remote, normalized, err := classifyGitTarget("ftp://user:wave9-canary@example.test/repo.git") + require.Error(t, err) + assert.False(t, remote) + assert.Empty(t, normalized) + assert.NotContains(t, err.Error(), "wave9-canary") +} + +func TestClassifyGitTarget_LocalPathContainingColonRemainsLocal(t *testing.T) { + remote, normalized, err := classifyGitTarget("repo:with-colon") + require.NoError(t, err) + assert.False(t, remote) + assert.Equal(t, "repo:with-colon", normalized) +} + +func TestGitSource_Validate_NonexistentRelativeTargetDoesNotOpenParentRepo(t *testing.T) { + dir, _ := initTestRepo(t, map[string]string{"README.md": "parent repo"}) + t.Chdir(dir) + + s := New("definitely-not-existing-wave9") + err := s.Validate(context.Background()) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to inspect git target") + assert.Nil(t, s.repo, "a typo must never silently select and scan the parent repository") +} + +func TestGitSource_Validate_LocalFileTargetIsRejected(t *testing.T) { + dir, _ := initTestRepo(t, map[string]string{"README.md": "parent repo"}) + s := New(filepath.Join(dir, "README.md")) + + err := s.Validate(context.Background()) + require.Error(t, err) + assert.Contains(t, err.Error(), "not a directory") + assert.Nil(t, s.repo) +} + +func TestGitSource_Validate_UppercaseHTTPSSchemeNeverLeaksUserinfo(t *testing.T) { + const canary = "wave9-canary-not-a-secret" + s := New("HTTPS://user:" + canary + "@127.0.0.1:1/repo.git") + + err := s.Validate(context.Background()) + require.Error(t, err) + assert.NotContains(t, err.Error(), canary) + assert.NotContains(t, err.Error(), "user:") + assert.Empty(t, s.tmpDir, "failed remote clone must clean its temporary directory") +} + func TestGitSource_Close_RemovesTmpDir(t *testing.T) { // Simulate a cloned repo by setting tmpDir manually. tmpDir := t.TempDir() @@ -488,6 +553,99 @@ func TestGitSource_Close_NoTmpDir(t *testing.T) { assert.NoError(t, err) } +func TestGitSource_Close_FailedCleanupRemainsRetryable(t *testing.T) { + tmpDir := t.TempDir() + cleanupErr := errors.New("synthetic cleanup failure") + s := &GitSource{ + tmpDir: tmpDir, + removeAll: func(string) error { return cleanupErr }, + } + + err := s.Close() + require.ErrorIs(t, err, cleanupErr) + assert.Equal(t, tmpDir, s.tmpDir, "failed cleanup must retain ownership for a retry") + + s.removeAll = os.RemoveAll + require.NoError(t, s.Close()) + assert.Empty(t, s.tmpDir) + _, err = os.Stat(tmpDir) + assert.True(t, os.IsNotExist(err)) +} + +func TestGitSource_ResolveCommitHash_CancellationStopsEnumeration(t *testing.T) { + dir, repo := initTestRepo(t, map[string]string{"base.txt": "base"}) + head, err := repo.Head() + require.NoError(t, err) + basePrefix := head.Hash().String()[:8] + for i := range 20 { + addCommit(t, dir, repo, map[string]string{ + fmt.Sprintf("file-%d.txt", i): fmt.Sprintf("content-%d", i), + }, fmt.Sprintf("commit-%d", i)) + } + + s := New(dir) + require.NoError(t, s.openLocal()) + ctx := &cancelAfterErrChecks{Context: context.Background(), remaining: 2} + + _, err = s.resolveCommitHash(ctx, basePrefix) + require.ErrorIs(t, err, context.Canceled) +} + +func TestIsAncestorContext_CancellationStopsHistoryWalk(t *testing.T) { + dir, repo := initTestRepo(t, map[string]string{"base.txt": "base"}) + baseRef, err := repo.Head() + require.NoError(t, err) + base, err := repo.CommitObject(baseRef.Hash()) + require.NoError(t, err) + + for i := range 20 { + addCommit(t, dir, repo, map[string]string{ + fmt.Sprintf("history-%d.txt", i): fmt.Sprintf("content-%d", i), + }, fmt.Sprintf("history-%d", i)) + } + headRef, err := repo.Head() + require.NoError(t, err) + head, err := repo.CommitObject(headRef.Hash()) + require.NoError(t, err) + + ctx := &cancelAfterErrChecks{Context: context.Background(), remaining: 2} + _, err = isAncestorContext(ctx, base, head) + require.ErrorIs(t, err, context.Canceled) +} + +func TestGitSource_Validate_RemoteCloneCancellationIsPromptAndCleansUp(t *testing.T) { + requestStarted := make(chan struct{}) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + select { + case <-requestStarted: + default: + close(requestStarted) + } + <-r.Context().Done() + })) + defer server.Close() + + s := New(server.URL + "/repo.git") + ctx, cancel := context.WithCancel(context.Background()) + done := make(chan error, 1) + go func() { done <- s.Validate(ctx) }() + + select { + case <-requestStarted: + case <-time.After(2 * time.Second): + t.Fatal("clone did not start an HTTP request") + } + cancel() + + select { + case err := <-done: + require.ErrorIs(t, err, context.Canceled) + assert.Empty(t, s.tmpDir, "cancelled clone must clean up its temporary directory") + case <-time.After(2 * time.Second): + t.Fatal("remote validation did not stop promptly after cancellation") + } +} + func TestSafeDisplayURL_StripsCredentials(t *testing.T) { // fakeToken is a non-secret placeholder used only to prove redaction. const fakeToken = "ghp_FAKEtoken1234567890" @@ -609,7 +767,7 @@ func TestGitSource_Chunks_RepositoryMetadataHasNoCredential(t *testing.T) { // credential-stripped remote form that should appear in metadata. s := New(dir) s.displayTarget = SafeDisplayURL("https://user:" + fakeToken + "@github.com/org/repo.git") - require.NoError(t, s.Validate()) + require.NoError(t, s.Validate(context.Background())) ctx := context.Background() var sawChunk bool @@ -637,7 +795,7 @@ func TestGitSource_Chunks_FullHistory_AttributesIntroducingCommit(t *testing.T) addCommit(t, dir, repo, map[string]string{"b.txt": "bbb"}, "third commit") s := New(dir) - require.NoError(t, s.Validate()) + require.NoError(t, s.Validate(context.Background())) ctx := context.Background() var configCommit string @@ -662,7 +820,7 @@ func TestGitSource_Chunks_FullHistory_ReportsBothPathsForIdenticalContent(t *tes }, "add duplicate content under a different path") s := New(dir) - require.NoError(t, s.Validate()) + require.NoError(t, s.Validate(context.Background())) ctx := context.Background() var files []string @@ -699,7 +857,7 @@ func TestGitSource_Chunks_Branch_ScansRequestedBranchLocally(t *testing.T) { addCommit(t, dir, repo, map[string]string{"main-only.txt": "main content"}, "main commit") s := New(dir, WithBranch("feature")) - require.NoError(t, s.Validate()) + require.NoError(t, s.Validate(context.Background())) ctx := context.Background() var files []string @@ -718,7 +876,7 @@ func TestGitSource_Validate_Branch_NotFound_ReturnsError(t *testing.T) { dir, _ := initTestRepo(t, map[string]string{"a.txt": "content"}) s := New(dir, WithBranch("does-not-exist")) - err := s.Validate() + err := s.Validate(context.Background()) require.Error(t, err) assert.Contains(t, err.Error(), "does-not-exist") } @@ -733,7 +891,7 @@ func TestGitSource_Validate_SinceCommit_AbbreviatedHash_Resolves(t *testing.T) { addCommit(t, dir, repo, map[string]string{"b.txt": "more"}, "second commit") s := New(dir, WithSinceCommit(shortHash)) - require.NoError(t, s.Validate(), "an abbreviated since-commit hash should resolve, not fail as 'not found'") + require.NoError(t, s.Validate(context.Background()), "an abbreviated since-commit hash should resolve, not fail as 'not found'") ctx := context.Background() var files []string @@ -749,7 +907,7 @@ func TestGitSource_Validate_SinceCommit_TooShort_ReturnsClearError(t *testing.T) dir, _ := initTestRepo(t, map[string]string{"a.txt": "content"}) s := New(dir, WithSinceCommit("ab")) - err := s.Validate() + err := s.Validate(context.Background()) require.Error(t, err) assert.Contains(t, err.Error(), "since-commit") assert.Contains(t, err.Error(), "too short") @@ -759,7 +917,7 @@ func TestGitSource_Validate_SinceCommit_NonHex_ReturnsClearError(t *testing.T) { dir, _ := initTestRepo(t, map[string]string{"a.txt": "content"}) s := New(dir, WithSinceCommit("zzzzzzz")) - err := s.Validate() + err := s.Validate(context.Background()) require.Error(t, err) assert.Contains(t, err.Error(), "not a valid commit hash") } @@ -791,7 +949,7 @@ func TestGitSource_Chunks_SinceCommitWithSince_HonorsSince(t *testing.T) { // since-commit excludes base; --since additionally drops the 2024 commit. cutoff := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) s := New(dir, WithSinceCommit(base.String()), WithSince(cutoff)) - require.NoError(t, s.Validate()) + require.NoError(t, s.Validate(context.Background())) ctx := context.Background() var files []string @@ -817,7 +975,7 @@ func TestGitSource_ResolveBranch_DetachedHead_ReportsShortCommit(t *testing.T) { require.NoError(t, wt.Checkout(&gogit.CheckoutOptions{Hash: baseHash})) s := New(dir) - require.NoError(t, s.Validate()) + require.NoError(t, s.Validate(context.Background())) branch := s.resolveBranch() assert.True(t, strings.HasPrefix(branch, "detached@"), diff --git a/internal/source/s3/s3.go b/internal/source/s3/s3.go index 3db14c4..6a119fc 100644 --- a/internal/source/s3/s3.go +++ b/internal/source/s3/s3.go @@ -23,11 +23,8 @@ import ( // defaultMaxFileSize is the maximum object size to scan (10 MB). const defaultMaxFileSize int64 = 10 * 1024 * 1024 -// validateTimeout bounds the network calls made by Validate. The -// source.Source interface's Validate() method takes no context.Context -// parameter, so the caller's own cancellation cannot be threaded through -// here; a bounded timeout at least prevents an unreachable/misconfigured -// bucket from hanging Validate indefinitely. +// validateTimeout bounds validation when the caller provides no earlier +// deadline. An earlier caller deadline or cancellation always wins. const validateTimeout = 30 * time.Second // s3Client defines the subset of the S3 API used by S3Source. @@ -100,12 +97,12 @@ func (s *S3Source) captureErr(err error) { // Validate checks that the S3 bucket is accessible. // It initializes the AWS client if not already set and performs a HeadBucket call. -func (s *S3Source) Validate() error { +func (s *S3Source) Validate(ctx context.Context) error { if s.bucket == "" { return fmt.Errorf("s3 bucket name is required") } - ctx, cancel := context.WithTimeout(context.Background(), validateTimeout) + ctx, cancel := context.WithTimeout(ctx, validateTimeout) defer cancel() if err := s.ensureClient(ctx); err != nil { diff --git a/internal/source/s3/s3_test.go b/internal/source/s3/s3_test.go index 952776e..1833a8f 100644 --- a/internal/source/s3/s3_test.go +++ b/internal/source/s3/s3_test.go @@ -7,6 +7,7 @@ import ( "strconv" "strings" "testing" + "time" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3/types" @@ -18,10 +19,13 @@ import ( // mockS3Client is a minimal mock for the s3Client interface. type mockS3Client struct { - objects []types.Object - data map[string]string - headErr error - listErr error + objects []types.Object + data map[string]string + headErr error + listErr error + headBlock bool + headDeadline time.Time + headHasDeadline bool } func (m *mockS3Client) ListObjectsV2(_ context.Context, input *s3.ListObjectsV2Input, _ ...func(*s3.Options)) (*s3.ListObjectsV2Output, error) { @@ -51,7 +55,12 @@ func (m *mockS3Client) GetObject(_ context.Context, input *s3.GetObjectInput, _ }, nil } -func (m *mockS3Client) HeadBucket(_ context.Context, _ *s3.HeadBucketInput, _ ...func(*s3.Options)) (*s3.HeadBucketOutput, error) { +func (m *mockS3Client) HeadBucket(ctx context.Context, _ *s3.HeadBucketInput, _ ...func(*s3.Options)) (*s3.HeadBucketOutput, error) { + m.headDeadline, m.headHasDeadline = ctx.Deadline() + if m.headBlock { + <-ctx.Done() + return nil, ctx.Err() + } return &s3.HeadBucketOutput{}, m.headErr } @@ -104,7 +113,7 @@ func TestWithMaxFileSize_ValidValue_Applied(t *testing.T) { func TestS3Source_Validate_EmptyBucket_ReturnsError(t *testing.T) { s := New("") - err := s.Validate() + err := s.Validate(context.Background()) require.Error(t, err) assert.Contains(t, err.Error(), "bucket name is required") } @@ -112,7 +121,32 @@ func TestS3Source_Validate_EmptyBucket_ReturnsError(t *testing.T) { func TestS3Source_Validate_AccessibleBucket_ReturnsNil(t *testing.T) { s := New("my-bucket") s.client = &mockS3Client{} - assert.NoError(t, s.Validate()) + assert.NoError(t, s.Validate(context.Background())) +} + +func TestS3Source_Validate_AppliesDefaultDeadline(t *testing.T) { + mock := &mockS3Client{} + s := New("my-bucket") + s.client = mock + started := time.Now() + + require.NoError(t, s.Validate(context.Background())) + require.True(t, mock.headHasDeadline) + assert.WithinDuration(t, started.Add(validateTimeout), mock.headDeadline, time.Second) +} + +func TestS3Source_Validate_CallerDeadlineCancelsRequest(t *testing.T) { + mock := &mockS3Client{headBlock: true} + s := New("my-bucket") + s.client = mock + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond) + defer cancel() + started := time.Now() + + err := s.Validate(ctx) + require.ErrorIs(t, err, context.DeadlineExceeded) + assert.Less(t, time.Since(started), time.Second) + assert.WithinDuration(t, started.Add(20*time.Millisecond), mock.headDeadline, 50*time.Millisecond) } func TestS3Source_Chunks_SendsTextObjects(t *testing.T) { diff --git a/internal/source/slack/client.go b/internal/source/slack/client.go index e045392..3f231a8 100644 --- a/internal/source/slack/client.go +++ b/internal/source/slack/client.go @@ -7,6 +7,7 @@ package slack import ( "context" + "io" "github.com/slack-go/slack" ) @@ -16,5 +17,7 @@ import ( type slackClient interface { GetConversationsContext(ctx context.Context, params *slack.GetConversationsParameters) ([]slack.Channel, string, error) GetConversationHistoryContext(ctx context.Context, params *slack.GetConversationHistoryParameters) (*slack.GetConversationHistoryResponse, error) + GetFileInfoContext(ctx context.Context, fileID string, count, page int) (*slack.File, []slack.Comment, *slack.Paging, error) + GetFileContext(ctx context.Context, downloadURL string, writer io.Writer) error AuthTestContext(ctx context.Context) (*slack.AuthTestResponse, error) } diff --git a/internal/source/slack/options.go b/internal/source/slack/options.go index 2246c32..5c6f993 100644 --- a/internal/source/slack/options.go +++ b/internal/source/slack/options.go @@ -38,22 +38,32 @@ func WithIncludeDMs(include bool) Option { } } -// WithIncludeFiles requests scanning of file content. -// -// NOTE: Slack file scanning is not yet implemented. This option is currently a -// no-op for actual scanning behavior (only message text is scanned); enabling -// it causes a warning to be logged. See ROADMAP and the planned-feature note in slack.go. +// WithIncludeFiles enables bounded scanning of text-like files attached to +// messages. The token needs Slack's files:read scope. func WithIncludeFiles(include bool) Option { return func(s *SlackSource) { s.includeFiles = include } } -// WithRateLimit sets the Slack API rate limit in requests per second. +// WithMaxFileSize sets the maximum attachment size buffered for scanning. +func WithMaxFileSize(size int64) Option { + return func(s *SlackSource) { + if size > 0 { + s.maxFileSize = size + } + } +} + +// WithRateLimit overrides each operation-scoped Slack limiter with the same +// requests-per-second ceiling. Defaults remain operation-specific. func WithRateLimit(rps float64) Option { return func(s *SlackSource) { if rps > 0 { - s.rateLimit = rps + s.historyRateLimit = rps + s.listRateLimit = rps + s.fileInfoRateLimit = rps + s.fileDownloadRateLimit = rps } } } diff --git a/internal/source/slack/slack.go b/internal/source/slack/slack.go index 4c9b190..385d257 100644 --- a/internal/source/slack/slack.go +++ b/internal/source/slack/slack.go @@ -2,13 +2,21 @@ package slack import ( + "bytes" "context" "errors" "fmt" + "io" "log/slog" + "mime" + "net/http" + "net/url" + "path" "strconv" "strings" "time" + "unicode" + "unicode/utf8" "github.com/slack-go/slack" "golang.org/x/time/rate" @@ -18,41 +26,47 @@ import ( ) const ( - // defaultRateLimit is a conservative default request rate for the Slack - // Web API's conversations.list/conversations.history methods. Slack's - // actual enforced tiers for these endpoints are commonly much stricter - // than a naive guess, so this default favors reliability over speed; - // callers with a higher-tier app/token can raise it via --rate-limit / - // WithRateLimit. Combined with the 429 retry handling below, this keeps - // scans from silently truncating on the first rate-limit response. - defaultRateLimit = 1.0 - defaultBufferSize = 100 + // DefaultRateLimit matches Slack's lowest published conversations.history + // contract for newly distributed non-Marketplace apps: one request/minute. + // Marketplace and internal customer-built apps can raise it explicitly. + DefaultRateLimit = 1.0 / 60.0 + defaultListRateLimit = 20.0 / 60.0 + defaultFileRateLimit = 100.0 / 60.0 + defaultBufferSize = 100 + defaultMaxFileSize = 10 * 1024 * 1024 + defaultHistoryLimit = 15 + validationTimeout = 10 * time.Second + slackRequestTimeout = 30 * time.Second // maxRateLimitRetries bounds how many consecutive HTTP 429 responses a // single page fetch will absorb before giving up, preventing an // unbounded retry loop against a workspace that never recovers. maxRateLimitRetries = 5 + maxSlackPages = 10_000 // defaultRetryAfter is used when a *slack.RateLimitedError reports a // non-positive RetryAfter duration, as a safe fallback backoff. - defaultRetryAfter = time.Second + defaultRetryAfter = time.Second + maximumRetryAfterWait = 2 * time.Minute ) // SlackSource scans messages in a Slack workspace for leaked secrets. type SlackSource struct { - token string - channels []string - excludeChannels []string - since time.Time - includeDMs bool - // includeFiles is accepted for forward-compatibility but is currently a - // no-op: Slack file scanning is not yet implemented (only message text is - // scanned). See the planned-feature note in processChannel below. - includeFiles bool - rateLimit float64 - bufferSize int - client slackClient - newClient func(token string) slackClient + token string + channels []string + excludeChannels []string + since time.Time + includeDMs bool + includeFiles bool + maxFileSize int64 + historyRateLimit float64 + listRateLimit float64 + fileInfoRateLimit float64 + fileDownloadRateLimit float64 + bufferSize int + client slackClient + newClient func(token string) slackClient + skippedAttachments int // err records the first terminal failure that aborted scanning (channel // listing / auth / pagination). It is written only by the Chunks goroutine, @@ -65,21 +79,34 @@ type SlackSource struct { // defaultNewClient creates a real Slack API client. func defaultNewClient(token string) slackClient { - return slack.New(token) + return slack.New(token, slack.OptionHTTPClient(newSlackHTTPClient())) +} + +func newSlackHTTPClient() *http.Client { + return &http.Client{ + CheckRedirect: rejectSlackRedirect, + Timeout: slackRequestTimeout, + } +} + +func rejectSlackRedirect(_ *http.Request, _ []*http.Request) error { + return http.ErrUseLastResponse } // New creates a new SlackSource for the given workspace token. // Use functional options to configure channel filtering, rate limits, etc. func New(token string, opts ...Option) *SlackSource { s := &SlackSource{ - token: token, - includeDMs: false, - // includeFiles defaults to false: file scanning is not implemented yet, - // so advertising it on by default would be misleading. - includeFiles: false, - rateLimit: defaultRateLimit, - bufferSize: defaultBufferSize, - newClient: defaultNewClient, + token: token, + includeDMs: false, + includeFiles: false, + maxFileSize: defaultMaxFileSize, + historyRateLimit: DefaultRateLimit, + listRateLimit: defaultListRateLimit, + fileInfoRateLimit: defaultFileRateLimit, + fileDownloadRateLimit: defaultFileRateLimit, + bufferSize: defaultBufferSize, + newClient: defaultNewClient, } for _, opt := range opts { opt(s) @@ -104,10 +131,10 @@ func (s *SlackSource) Err() error { // is called only from the single Chunks goroutine, before close(ch), so a plain // field write is safe (the channel close/drain publishes it to Err's reader). // As defense-in-depth against a client library that echoes the token, any -// occurrence of the workspace token is stripped from the stored message (and, -// when present, the error is flattened so the token cannot survive in the unwrap -// chain either). Context cancellation is never recorded because it is reported -// through the context, not Err. +// occurrence of the workspace token is stripped from the stored message. Every +// provider error is flattened so a wrapper cannot hide credentials in Unwrap. +// Context cancellation is never recorded because it is reported through the +// context, not Err. func (s *SlackSource) captureErr(err error) { if err == nil || s.err != nil { return @@ -115,24 +142,44 @@ func (s *SlackSource) captureErr(err error) { if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return } - if s.token != "" && strings.Contains(err.Error(), s.token) { - s.err = errors.New(strings.ReplaceAll(err.Error(), s.token, "***")) - return + s.err = s.safeSlackError(err) +} + +func (s *SlackSource) safeSlackError(err error) error { + if err == nil { + return nil + } + message := err.Error() + if s.token != "" { + message = strings.ReplaceAll(message, s.token, "***") } - s.err = err + return errors.New(message) } -// Validate checks that the Slack token is valid by calling AuthTest. -func (s *SlackSource) Validate() error { +// Validate checks that the Slack token is valid by calling AuthTest. The +// operation is bounded even when the caller supplies no deadline, while an +// earlier caller deadline or cancellation always wins. +func (s *SlackSource) Validate(ctx context.Context) error { if s.token == "" { return fmt.Errorf("slack token is required") } + if err := ctx.Err(); err != nil { + return err + } s.ensureClient() - _, err := s.client.AuthTestContext(context.Background()) + validateCtx, cancel := context.WithTimeout(ctx, validationTimeout) + defer cancel() + _, err := s.client.AuthTestContext(validateCtx) if err != nil { - return fmt.Errorf("slack auth test failed: %w", err) + if errors.Is(err, context.Canceled) { + return fmt.Errorf("slack auth test failed: %w", context.Canceled) + } + if errors.Is(err, context.DeadlineExceeded) { + return fmt.Errorf("slack auth test failed: %w", context.DeadlineExceeded) + } + return fmt.Errorf("slack auth test failed: %w", s.safeSlackError(err)) } return nil @@ -141,24 +188,32 @@ func (s *SlackSource) Validate() error { // Chunks lists channels in the workspace and sends message contents over a channel. // The channel is closed when all messages have been processed or the context is cancelled. func (s *SlackSource) Chunks(ctx context.Context) <-chan source.Chunk { - ch := make(chan source.Chunk, s.bufferSize) + bufferSize := s.bufferSize + if s.includeFiles { + bufferSize = 0 + } + ch := make(chan source.Chunk, bufferSize) go func() { defer close(ch) + s.skippedAttachments = 0 + defer func() { + if s.skippedAttachments > 0 { + slog.Warn("Slack scan completed with skipped attachments", + slog.Int("skipped_attachments", s.skippedAttachments)) + } + }() s.ensureClient() - // File scanning is advertised via WithIncludeFiles but is not yet - // implemented. Warn loudly instead of silently ignoring the request so - // the behavior is honest. See the planned-feature note in processChannel. - if s.includeFiles { - slog.Warn("slack file scanning requested but not yet implemented; scanning message text only") - } - - limiter := rate.NewLimiter(rate.Limit(s.rateLimit), 1) + listLimiter := rate.NewLimiter(rate.Limit(s.listRateLimit), 1) + historyLimiter := rate.NewLimiter(rate.Limit(s.historyRateLimit), 1) + fileInfoLimiter := rate.NewLimiter(rate.Limit(s.fileInfoRateLimit), 1) + fileDownloadLimiter := rate.NewLimiter(rate.Limit(s.fileDownloadRateLimit), 1) + seenFiles := make(map[string]struct{}) - channels, err := s.listChannels(ctx, limiter) + channels, err := s.listChannels(ctx, listLimiter) if err != nil { - slog.Error("slack channel listing failed", "error", err) + slog.Error("slack channel listing failed", "error", s.safeSlackError(err)) s.captureErr(fmt.Errorf("slack channel listing failed: %w", err)) return } @@ -172,7 +227,10 @@ func (s *SlackSource) Chunks(ctx context.Context) <-chan source.Chunk { default: } - s.processChannel(ctx, ch, limiter, channel) + s.processChannel(ctx, ch, historyLimiter, fileInfoLimiter, fileDownloadLimiter, channel, seenFiles) + if s.err != nil { + return + } } }() return ch @@ -192,7 +250,10 @@ func (s *SlackSource) listChannels(ctx context.Context, limiter *rate.Limiter) ( cursor := "" retries := 0 - for { + for page := 1; ; page++ { + if page > maxSlackPages { + return allChannels, fmt.Errorf("slack list conversations exceeded %d pages", maxSlackPages) + } select { case <-ctx.Done(): return allChannels, ctx.Err() @@ -234,9 +295,13 @@ func (s *SlackSource) listChannels(ctx context.Context, limiter *rate.Limiter) ( allChannels = append(allChannels, channels...) + nextCursor = strings.TrimSpace(nextCursor) if nextCursor == "" { break } + if nextCursor == cursor { + return allChannels, fmt.Errorf("slack list conversations returned a stalled cursor") + } cursor = nextCursor } @@ -260,6 +325,9 @@ func waitRetryAfter(ctx context.Context, d time.Duration) error { if d <= 0 { d = defaultRetryAfter } + if d > maximumRetryAfterWait { + return fmt.Errorf("slack Retry-After exceeds the bounded wait limit") + } timer := time.NewTimer(d) defer timer.Stop() select { @@ -308,105 +376,371 @@ func (s *SlackSource) filterChannels(channels []slack.Channel) []slack.Channel { } // processChannel reads message history for a single channel and emits chunks. -func (s *SlackSource) processChannel(ctx context.Context, ch chan<- source.Chunk, limiter *rate.Limiter, channel slack.Channel) { +func (s *SlackSource) processChannel( + ctx context.Context, + ch chan<- source.Chunk, + historyLimiter *rate.Limiter, + fileInfoLimiter *rate.Limiter, + fileDownloadLimiter *rate.Limiter, + channel slack.Channel, + seenFiles ...map[string]struct{}, +) { + seen := make(map[string]struct{}) + if len(seenFiles) > 0 && seenFiles[0] != nil { + seen = seenFiles[0] + } cursor := "" - retries := 0 - for { - select { - case <-ctx.Done(): + for page := 1; ; page++ { + if page > maxSlackPages { + s.captureErr(fmt.Errorf("slack conversation history for channel %s exceeded %d pages", channel.ID, maxSlackPages)) + return + } + resp, ok := s.fetchConversationHistoryPage(ctx, historyLimiter, channel, cursor) + if !ok || !s.processSlackMessages(ctx, ch, fileInfoLimiter, fileDownloadLimiter, channel, resp.Messages, seen) { return - default: } - if err := limiter.Wait(ctx); err != nil { - slog.Warn("slack rate limiter wait failed", "channel", channel.ID, "error", err) + if !resp.HasMore { return } + nextCursor := strings.TrimSpace(resp.ResponseMetaData.NextCursor) + if nextCursor == "" || nextCursor == cursor { + s.captureErr(fmt.Errorf("slack conversation history for channel %s returned a stalled cursor", channel.ID)) + return + } + cursor = nextCursor + } +} - params := &slack.GetConversationHistoryParameters{ - ChannelID: channel.ID, - Cursor: cursor, - Limit: 200, +func (s *SlackSource) fetchConversationHistoryPage( + ctx context.Context, + limiter *rate.Limiter, + channel slack.Channel, + cursor string, +) (*slack.GetConversationHistoryResponse, bool) { + params := s.historyParameters(channel.ID, cursor) + for retries := 0; ; retries++ { + if err := limiter.Wait(ctx); err != nil { + slog.Warn("slack rate limiter wait failed", "channel", channel.ID, "error", s.safeSlackError(err)) + s.captureErr(fmt.Errorf("slack conversation history for channel %s rate limiter wait: %w", channel.ID, err)) + return nil, false + } + response, err := s.client.GetConversationHistoryContext(ctx, params) + if err == nil { + return response, true } + if !s.retryConversationHistory(ctx, channel.ID, retries+1, err) { + return nil, false + } + } +} + +func (s *SlackSource) historyParameters(channelID, cursor string) *slack.GetConversationHistoryParameters { + params := &slack.GetConversationHistoryParameters{ChannelID: channelID, Cursor: cursor, Limit: defaultHistoryLimit} + if !s.since.IsZero() { + params.Oldest = formatSlackTimestamp(s.since) + } + return params +} - // Push the since filter down to the API via the "oldest" parameter so - // older messages are never transferred. The client-side check below - // remains as a correctness backstop for boundary timestamps. - if !s.since.IsZero() { - params.Oldest = formatSlackTimestamp(s.since) +func (s *SlackSource) retryConversationHistory(ctx context.Context, channelID string, attempt int, err error) bool { + retryAfter, rateLimited := rateLimitRetryAfter(err) + if !rateLimited { + slog.Warn("slack conversation history failed", "channel", channelID, "error", s.safeSlackError(err)) + s.captureErr(fmt.Errorf("slack conversation history for channel %s: %w", channelID, err)) + return false + } + if attempt > maxRateLimitRetries { + slog.Warn("slack conversation history rate limited, giving up", + "channel", channelID, "attempts", attempt, "error", s.safeSlackError(err)) + s.captureErr(fmt.Errorf("slack conversation history for channel %s: rate limited after %d retries: %w", channelID, maxRateLimitRetries, err)) + return false + } + slog.Warn("slack conversation history rate limited, retrying", + "channel", channelID, "retry_after", retryAfter, "attempt", attempt) + if waitErr := waitRetryAfter(ctx, retryAfter); waitErr != nil { + slog.Warn("slack rate limiter wait failed", "channel", channelID, "error", s.safeSlackError(waitErr)) + s.captureErr(fmt.Errorf("slack conversation history for channel %s retry wait: %w", channelID, waitErr)) + return false + } + return true +} + +func (s *SlackSource) processSlackMessages( + ctx context.Context, + output chan<- source.Chunk, + fileInfoLimiter, fileDownloadLimiter *rate.Limiter, + channel slack.Channel, + messages []slack.Message, + seen map[string]struct{}, +) bool { + for _, message := range messages { + if ctx.Err() != nil { + return false + } + if !s.since.IsZero() && parseSlackTimestamp(message.Timestamp).Before(s.since) { + continue } + if !s.processSlackMessage(ctx, output, fileInfoLimiter, fileDownloadLimiter, channel, message, seen) { + return false + } + } + return true +} + +func (s *SlackSource) processSlackMessage( + ctx context.Context, + output chan<- source.Chunk, + fileInfoLimiter, fileDownloadLimiter *rate.Limiter, + channel slack.Channel, + message slack.Message, + seen map[string]struct{}, +) bool { + metadata := finding.SourceMetadata{ + SourceType: "slack", Channel: channel.ID, ChannelName: channel.Name, + MessageUser: message.User, MessageTS: message.Timestamp, ThreadTS: message.ThreadTimestamp, + } + if message.Text != "" && !sendSlackChunk(ctx, output, source.Chunk{Data: []byte(message.Text), SourceMetadata: metadata}) { + return false + } + if !s.includeFiles { + return true + } + return s.processSlackFiles(ctx, output, fileInfoLimiter, fileDownloadLimiter, channel, message, metadata, seen) +} - resp, err := s.client.GetConversationHistoryContext(ctx, params) +func (s *SlackSource) processSlackFiles( + ctx context.Context, + output chan<- source.Chunk, + fileInfoLimiter, fileDownloadLimiter *rate.Limiter, + channel slack.Channel, + message slack.Message, + metadata finding.SourceMetadata, + seen map[string]struct{}, +) bool { + for _, attached := range message.Files { + if attached.ID == "" { + continue + } + if _, duplicate := seen[attached.ID]; duplicate { + continue + } + seen[attached.ID] = struct{}{} + chunk, ok, err := s.fileChunk(ctx, fileInfoLimiter, fileDownloadLimiter, channel, message, attached, metadata) if err != nil { - if retryAfter, ok := rateLimitRetryAfter(err); ok { - retries++ - if retries > maxRateLimitRetries { - slog.Warn("slack conversation history rate limited, giving up", - "channel", channel.ID, "attempts", retries, "error", err) - return - } - slog.Warn("slack conversation history rate limited, retrying", - "channel", channel.ID, "retry_after", retryAfter, "attempt", retries) - if waitErr := waitRetryAfter(ctx, retryAfter); waitErr != nil { - slog.Warn("slack rate limiter wait failed", "channel", channel.ID, "error", waitErr) - return - } - continue + if ctx.Err() != nil { + return false } - slog.Warn("slack conversation history failed", "channel", channel.ID, "error", err) - return + s.skippedAttachments++ + slog.Warn("skipping unreadable Slack attachment", + "channel", channel.ID, + "message_ts", message.Timestamp, + "file_id", attached.ID, + "error", s.safeSlackError(err)) + continue } - retries = 0 + if ok && !sendSlackChunk(ctx, output, chunk) { + return false + } + } + return true +} - for _, msg := range resp.Messages { - select { - case <-ctx.Done(): - return - default: - } +func sendSlackChunk(ctx context.Context, output chan<- source.Chunk, chunk source.Chunk) bool { + select { + case output <- chunk: + return true + case <-ctx.Done(): + return false + } +} - // Apply since filter by parsing the message timestamp. - if !s.since.IsZero() { - msgTime := parseSlackTimestamp(msg.Timestamp) - if msgTime.Before(s.since) { - continue - } - } +func (s *SlackSource) fileChunk( + ctx context.Context, + fileInfoLimiter *rate.Limiter, + fileDownloadLimiter *rate.Limiter, + channel slack.Channel, + message slack.Message, + attached slack.File, + metadata finding.SourceMetadata, +) (source.Chunk, bool, error) { + info, err := s.fileInfo(ctx, fileInfoLimiter, attached) + if err != nil { + return source.Chunk{}, false, fmt.Errorf("slack file %s metadata: %w", attached.ID, err) + } + if info.Size > 0 && int64(info.Size) > s.maxFileSize { + slog.Debug("skipping oversized Slack attachment", "file_id", info.ID, "size", info.Size, "limit", s.maxFileSize) + return source.Chunk{}, false, nil + } + if !isTextLikeSlackFile(*info) { + return source.Chunk{}, false, nil + } + downloadURL := info.URLPrivateDownload + if downloadURL == "" { + downloadURL = info.URLPrivate + } + if err := validateSlackDownloadURL(downloadURL); err != nil { + return source.Chunk{}, false, fmt.Errorf("slack file %s download URL rejected: %w", info.ID, err) + } - if msg.Text == "" { - continue - } + contents, err := s.downloadFile(ctx, fileDownloadLimiter, downloadURL) + if err != nil { + if errors.Is(err, errSlackFileTooLarge) { + return source.Chunk{}, false, nil + } + return source.Chunk{}, false, fmt.Errorf("slack file %s download: %w", info.ID, err) + } + if len(contents) == 0 || !isProbablyText(contents) { + return source.Chunk{}, false, nil + } - // Planned (see ROADMAP): Slack file scanning. When s.includeFiles is - // honored, msg.Files (and each File.URLPrivate) should be downloaded - // and emitted as additional chunks here. Currently only msg.Text is - // scanned. + fileName := safeSlackPathSegment(info.Name, "attachment") + channelName := safeSlackPathSegment(strings.TrimPrefix(channel.Name, "#"), "channel") + metadata.FilePath = path.Join("slack", channelName, fileName) + metadata.MessageUser = message.User + return source.Chunk{Data: contents, SourceMetadata: metadata}, true, nil +} - select { - case ch <- source.Chunk{ - Data: []byte(msg.Text), - SourceMetadata: finding.SourceMetadata{ - SourceType: "slack", - Channel: channel.ID, - ChannelName: channel.Name, - MessageUser: msg.User, - MessageTS: msg.Timestamp, - ThreadTS: msg.ThreadTimestamp, - }, - }: - case <-ctx.Done(): - return +func safeSlackPathSegment(value, fallback string) string { + segment := path.Base(strings.ReplaceAll(value, "\\", "/")) + if segment == "." || segment == ".." || segment == "/" || segment == "" { + return fallback + } + var cleaned strings.Builder + for _, char := range segment { + if char < 0x20 || char == 0x7f { + cleaned.WriteByte('_') + continue + } + cleaned.WriteRune(char) + if cleaned.Len() >= 255 { + break + } + } + if cleaned.Len() == 0 { + return fallback + } + return cleaned.String() +} + +func (s *SlackSource) downloadFile(ctx context.Context, limiter *rate.Limiter, downloadURL string) ([]byte, error) { + for attempt := 0; attempt <= maxRateLimitRetries; attempt++ { + if err := limiter.Wait(ctx); err != nil { + return nil, fmt.Errorf("slack file download limiter wait: %w", err) + } + buffer := &boundedFileBuffer{remaining: s.maxFileSize} + err := s.client.GetFileContext(ctx, downloadURL, buffer) + if err == nil { + return buffer.Bytes(), nil + } + if errors.Is(err, errSlackFileTooLarge) { + return nil, err + } + retryAfter, limited := rateLimitRetryAfter(err) + if !limited || attempt == maxRateLimitRetries { + return nil, err + } + if err := waitRetryAfter(ctx, retryAfter); err != nil { + return nil, err + } + } + return nil, fmt.Errorf("slack file download retry budget exhausted") +} + +func (s *SlackSource) fileInfo(ctx context.Context, limiter *rate.Limiter, attached slack.File) (*slack.File, error) { + for attempt := 0; attempt <= maxRateLimitRetries; attempt++ { + if err := limiter.Wait(ctx); err != nil { + return nil, fmt.Errorf("slack file metadata limiter wait: %w", err) + } + info, _, _, err := s.client.GetFileInfoContext(ctx, attached.ID, 0, 0) + if err == nil { + if info == nil { + return nil, fmt.Errorf("slack returned empty file metadata") } + return info, nil + } + retryAfter, limited := rateLimitRetryAfter(err) + if !limited || attempt == maxRateLimitRetries { + return nil, err } + if err := waitRetryAfter(ctx, retryAfter); err != nil { + return nil, err + } + } + return nil, fmt.Errorf("slack file metadata retry budget exhausted") +} - if !resp.HasMore { - return +func isTextLikeSlackFile(file slack.File) bool { + rawType := strings.TrimSpace(file.Mimetype) + if rawType == "" { + return true + } + mimeType, _, err := mime.ParseMediaType(rawType) + if err != nil { + return false + } + mimeType = strings.ToLower(mimeType) + if strings.HasPrefix(mimeType, "text/") { + return true + } + switch mimeType { + case "application/json", "application/ld+json", "application/xml", "application/x-yaml", + "application/yaml", "application/toml", "application/javascript", "application/x-sh": + return true + default: + return false + } +} + +func isProbablyText(data []byte) bool { + if len(data) == 0 || !utf8.Valid(data) || bytes.IndexByte(data, 0) >= 0 { + return false + } + total, nonText := 0, 0 + for _, char := range string(data) { + total++ + if unicode.IsPrint(char) || char == '\n' || char == '\r' || char == '\t' { + continue + } + nonText++ + } + return total > 0 && nonText*100 <= total*5 +} + +func validateSlackDownloadURL(raw string) error { + parsed, err := url.Parse(raw) + if err != nil || parsed.Scheme != "https" || parsed.User != nil || parsed.Hostname() == "" { + return fmt.Errorf("expected an absolute credential-free HTTPS URL") + } + if !strings.EqualFold(parsed.Hostname(), "files.slack.com") || (parsed.Port() != "" && parsed.Port() != "443") { + return fmt.Errorf("host is not Slack's file-download endpoint") + } + return nil +} + +var errSlackFileTooLarge = errors.New("slack attachment exceeds configured maximum") + +type boundedFileBuffer struct { + bytes.Buffer + remaining int64 +} + +func (w *boundedFileBuffer) Write(input []byte) (int, error) { + if int64(len(input)) > w.remaining { + written := 0 + if w.remaining > 0 { + written, _ = w.Buffer.Write(input[:int(w.remaining)]) + w.remaining = 0 } - cursor = resp.ResponseMetaData.NextCursor + return written, errSlackFileTooLarge } + w.remaining -= int64(len(input)) + return w.Buffer.Write(input) } +var _ io.Writer = (*boundedFileBuffer)(nil) + // formatSlackTimestamp converts a time.Time to the Slack "oldest" parameter // format (Unix seconds with a fractional component, e.g. "1234567890.000000"). func formatSlackTimestamp(t time.Time) string { diff --git a/internal/source/slack/slack_test.go b/internal/source/slack/slack_test.go index 51320d4..c72a61b 100644 --- a/internal/source/slack/slack_test.go +++ b/internal/source/slack/slack_test.go @@ -1,8 +1,14 @@ package slack import ( + "bytes" "context" + "errors" "fmt" + "io" + "log/slog" + "net/http" + "net/http/httptest" "testing" "time" @@ -29,13 +35,25 @@ type mockHistoryPage struct { nextCursor string } +type hiddenSlackError struct{ cause error } + +func (e hiddenSlackError) Error() string { return "outer SDK failure" } +func (e hiddenSlackError) Unwrap() error { return e.cause } + // mockSlackClient is a minimal mock for the slackClient interface. type mockSlackClient struct { - channels []slack.Channel - messages map[string][]slack.Message - authErr error - listErr error - historyErr error + channels []slack.Channel + messages map[string][]slack.Message + authErr error + authWait bool + authCtx context.Context + listErr error + historyErr error + fileInfoErr error + downloadErr error + files map[string]slack.File + fileBodies map[string][]byte + downloadStarted chan string // listRateLimitedCalls, when > 0, makes that many upcoming calls to // GetConversationsContext return a *slack.RateLimitedError before @@ -58,15 +76,23 @@ type mockSlackClient struct { historyPages map[string]mockHistoryPage // listCalls and historyCalls count invocations, for assertions. - listCalls int - historyCalls int + listCalls int + historyCalls int + fileInfoCalls int + downloadCalls int // lastHistoryOldest records the Oldest parameter from the most recent // GetConversationHistoryContext call, for assertions. lastHistoryOldest string + lastHistoryLimit int } -func (m *mockSlackClient) AuthTestContext(_ context.Context) (*slack.AuthTestResponse, error) { +func (m *mockSlackClient) AuthTestContext(ctx context.Context) (*slack.AuthTestResponse, error) { + m.authCtx = ctx + if m.authWait { + <-ctx.Done() + return nil, ctx.Err() + } if m.authErr != nil { return nil, m.authErr } @@ -103,6 +129,7 @@ func (m *mockSlackClient) GetConversationsContext(_ context.Context, params *sla func (m *mockSlackClient) GetConversationHistoryContext(_ context.Context, params *slack.GetConversationHistoryParameters) (*slack.GetConversationHistoryResponse, error) { m.historyCalls++ m.lastHistoryOldest = params.Oldest + m.lastHistoryLimit = params.Limit if m.historyRateLimitedCalls > 0 { m.historyRateLimitedCalls-- @@ -143,6 +170,41 @@ func (m *mockSlackClient) GetConversationHistoryContext(_ context.Context, param }, nil } +func (m *mockSlackClient) GetFileInfoContext(_ context.Context, fileID string, _, _ int) (*slack.File, []slack.Comment, *slack.Paging, error) { + m.fileInfoCalls++ + if m.fileInfoErr != nil { + return nil, nil, nil, m.fileInfoErr + } + file, ok := m.files[fileID] + if !ok { + return nil, nil, nil, fmt.Errorf("file_not_found") + } + return &file, nil, &slack.Paging{}, nil +} + +func (m *mockSlackClient) GetFileContext(ctx context.Context, downloadURL string, writer io.Writer) error { + m.downloadCalls++ + if m.downloadStarted != nil { + select { + case m.downloadStarted <- downloadURL: + case <-ctx.Done(): + return ctx.Err() + } + } + if err := ctx.Err(); err != nil { + return err + } + if m.downloadErr != nil { + return m.downloadErr + } + body, ok := m.fileBodies[downloadURL] + if !ok { + return fmt.Errorf("download_not_found") + } + _, err := io.Copy(writer, bytes.NewReader(body)) + return err +} + func TestSlackSource_Type_ReturnsSlack(t *testing.T) { s := New("xoxb-test-token") assert.Equal(t, "slack", s.Type()) @@ -152,7 +214,7 @@ func TestSlackSource_Validate_ValidToken_ReturnsNoError(t *testing.T) { s := New("xoxb-test-token") s.client = &mockSlackClient{} - err := s.Validate() + err := s.Validate(context.Background()) assert.NoError(t, err) } @@ -162,7 +224,7 @@ func TestSlackSource_Validate_InvalidToken_ReturnsError(t *testing.T) { authErr: fmt.Errorf("invalid_auth"), } - err := s.Validate() + err := s.Validate(context.Background()) require.Error(t, err) assert.Contains(t, err.Error(), "slack auth test failed") assert.Contains(t, err.Error(), "invalid_auth") @@ -171,11 +233,49 @@ func TestSlackSource_Validate_InvalidToken_ReturnsError(t *testing.T) { func TestSlackSource_Validate_EmptyToken_ReturnsError(t *testing.T) { s := New("") - err := s.Validate() + err := s.Validate(context.Background()) require.Error(t, err) assert.Contains(t, err.Error(), "slack token is required") } +func TestSlackSource_Validate_IsBoundedWithoutCallerDeadline(t *testing.T) { + mock := &mockSlackClient{} + s := New("xoxb-test-token") + s.client = mock + + started := time.Now() + require.NoError(t, s.Validate(context.Background())) + deadline, ok := mock.authCtx.Deadline() + require.True(t, ok, "AuthTest validation must always carry a deadline") + assert.Greater(t, deadline, started) + assert.LessOrEqual(t, deadline.Sub(started), validationTimeout+time.Second) +} + +func TestSlackSource_Validate_HonorsCallerCancellationAndRedactsToken(t *testing.T) { + const token = "xoxb-synthetic-validation-canary" + mock := &mockSlackClient{authWait: true} + s := New(token) + s.client = mock + + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond) + defer cancel() + started := time.Now() + err := s.Validate(ctx) + require.Error(t, err) + assert.ErrorIs(t, err, context.DeadlineExceeded) + assert.Less(t, time.Since(started), time.Second) + assert.NotContains(t, err.Error(), token) + + // A provider/library error that echoes the raw token must also be flattened + // before it can reach CLI stderr. + mock = &mockSlackClient{authErr: fmt.Errorf("invalid token %s", token)} + s.client = mock + err = s.Validate(context.Background()) + require.Error(t, err) + assert.False(t, errors.Is(err, context.Canceled)) + assert.NotContains(t, err.Error(), token) +} + func TestSlackSource_Chunks_SingleChannel_EmitsMessages(t *testing.T) { mock := &mockSlackClient{ channels: []slack.Channel{ @@ -205,6 +305,7 @@ func TestSlackSource_Chunks_SingleChannel_EmitsMessages(t *testing.T) { assert.Len(t, chunks, 2) assert.Contains(t, chunks, "here is my API_KEY=sk-abc123") assert.Contains(t, chunks, "another message with SECRET=xyz") + assert.Equal(t, defaultHistoryLimit, mock.lastHistoryLimit) // All chunks should reference channel C001. for _, m := range metas { assert.Equal(t, "C001", m) @@ -377,16 +478,22 @@ func TestSlackSource_New_IncludeFilesDefaultsFalse(t *testing.T) { assert.False(t, s.includeFiles) } -func TestSlackSource_Chunks_IncludeFiles_ScansTextOnly(t *testing.T) { - // File scanning is not implemented; enabling WithIncludeFiles must not - // change the emitted chunks (still text-only) and must not error. +func TestSlackSource_Chunks_IncludeFiles_ScansMessageAndBoundedTextAttachment(t *testing.T) { + const downloadURL = "https://files.slack.com/files-pri/T1-F1/config.env" mock := &mockSlackClient{ channels: []slack.Channel{ {GroupConversation: slack.GroupConversation{Conversation: slack.Conversation{ID: "C001"}, Name: "general"}}, }, messages: map[string][]slack.Message{ - "C001": {{Msg: slack.Msg{Text: "hello", User: "U001", Timestamp: "1700000001.000000"}}}, + "C001": {{Msg: slack.Msg{ + Text: "hello", User: "U001", Timestamp: "1700000001.000000", + Files: []slack.File{{ID: "F1"}}, + }}}, + }, + files: map[string]slack.File{ + "F1": {ID: "F1", Name: "config.env", Mimetype: "text/plain", Size: 20, URLPrivateDownload: downloadURL}, }, + fileBodies: map[string][]byte{downloadURL: []byte("TOKEN=synthetic-value")}, } s := New("xoxb-test-token", WithIncludeFiles(true), WithRateLimit(1000)) @@ -398,7 +505,328 @@ func TestSlackSource_Chunks_IncludeFiles_ScansTextOnly(t *testing.T) { chunks = append(chunks, string(chunk.Data)) } - assert.Equal(t, []string{"hello"}, chunks) + assert.Equal(t, []string{"hello", "TOKEN=synthetic-value"}, chunks) + assert.Equal(t, 1, mock.fileInfoCalls) + assert.Equal(t, 1, mock.downloadCalls) +} + +func TestSlackSource_Chunks_IncludeFiles_RejectsHostileDownloadURLBeforeClientCall(t *testing.T) { + const hostileURL = "https://files.slack.com.attacker.example/files-pri/T1-F1/config.env" + mock := &mockSlackClient{ + channels: []slack.Channel{ + {GroupConversation: slack.GroupConversation{Conversation: slack.Conversation{ID: "C001"}, Name: "general"}}, + }, + messages: map[string][]slack.Message{ + "C001": {{Msg: slack.Msg{Timestamp: "1700000001.000000", Files: []slack.File{{ID: "F1"}}}}}, + }, + files: map[string]slack.File{ + "F1": {ID: "F1", Name: "config.env", Mimetype: "text/plain", URLPrivateDownload: hostileURL}, + }, + fileBodies: map[string][]byte{hostileURL: []byte("TOKEN=must-not-be-downloaded")}, + } + + s := New("xoxb-test-token", WithIncludeFiles(true), WithRateLimit(1000)) + s.client = mock + for range s.Chunks(context.Background()) { + } + + assert.Equal(t, 1, mock.fileInfoCalls) + assert.Zero(t, mock.downloadCalls, "an untrusted attachment URL must be rejected before the authenticated download client is called") + assert.Equal(t, 1, s.skippedAttachments) + assert.NoError(t, s.Err(), "one rejected attachment must not abort the remaining workspace scan") +} + +func TestSlackSource_Chunks_IncludeFiles_SkipsOversizedBinaryAndDuplicateFiles(t *testing.T) { + const ( + textURL = "https://files.slack.com/files-pri/T1-F1/text" + binaryURL = "https://files.slack.com/files-pri/T1-F2/binary" + ) + mock := &mockSlackClient{ + channels: []slack.Channel{{GroupConversation: slack.GroupConversation{Conversation: slack.Conversation{ID: "C001"}, Name: "general"}}}, + messages: map[string][]slack.Message{"C001": { + {Msg: slack.Msg{Timestamp: "1700000001.000000", Files: []slack.File{{ID: "F1"}, {ID: "F2"}, {ID: "F3"}}}}, + {Msg: slack.Msg{Timestamp: "1700000002.000000", Files: []slack.File{{ID: "F1"}}}}, + }}, + files: map[string]slack.File{ + "F1": {ID: "F1", Name: "large.txt", Mimetype: "text/plain", Size: 100, URLPrivateDownload: textURL}, + "F2": {ID: "F2", Name: "image.bin", Mimetype: "application/octet-stream", Size: 3, URLPrivateDownload: binaryURL}, + "F3": {ID: "F3", Name: "nul.txt", Mimetype: "text/plain", Size: 3, URLPrivateDownload: textURL}, + }, + fileBodies: map[string][]byte{textURL: {'a', 0, 'b'}, binaryURL: {1, 2, 3}}, + } + s := New("xoxb-test-token", WithIncludeFiles(true), WithMaxFileSize(16), WithRateLimit(1000)) + s.client = mock + + var chunks []source.Chunk + for chunk := range s.Chunks(context.Background()) { + chunks = append(chunks, chunk) + } + assert.Empty(t, chunks) + assert.Equal(t, 3, mock.fileInfoCalls, "duplicate F1 must not be fetched twice") + assert.Equal(t, 1, mock.downloadCalls, "only the text-like, in-limit F3 reaches download") +} + +func TestSlackSource_Chunks_IncludeFiles_DownloadFailureIsSkippedAndLaterChannelsContinue(t *testing.T) { + const downloadURL = "https://files.slack.com/files-pri/T1-F1/config.env" + token := "xoxb-sensitive-fixture" + mock := &mockSlackClient{ + channels: []slack.Channel{ + {GroupConversation: slack.GroupConversation{Conversation: slack.Conversation{ID: "C001"}, Name: "general"}}, + {GroupConversation: slack.GroupConversation{Conversation: slack.Conversation{ID: "C002"}, Name: "security"}}, + }, + messages: map[string][]slack.Message{ + "C001": {{Msg: slack.Msg{Timestamp: "1700000001.000000", Files: []slack.File{{ID: "F1"}}}}}, + "C002": {{Msg: slack.Msg{Timestamp: "1700000002.000000", Text: "later channel secret"}}}, + }, + files: map[string]slack.File{"F1": {ID: "F1", Name: "config.env", Mimetype: "text/plain", URLPrivateDownload: downloadURL}}, + downloadErr: fmt.Errorf("download failed for %s", token), + } + var logs bytes.Buffer + previous := slog.Default() + slog.SetDefault(slog.New(slog.NewTextHandler(&logs, nil))) + t.Cleanup(func() { slog.SetDefault(previous) }) + + s := New(token, WithIncludeFiles(true), WithRateLimit(1000)) + s.client = mock + var chunks []source.Chunk + for chunk := range s.Chunks(context.Background()) { + chunks = append(chunks, chunk) + } + require.NoError(t, s.Err()) + require.Len(t, chunks, 1) + assert.Equal(t, "later channel secret", string(chunks[0].Data)) + assert.Equal(t, "C002", chunks[0].SourceMetadata.Channel) + assert.Equal(t, 2, mock.historyCalls) + assert.Equal(t, 1, s.skippedAttachments) + assert.NotContains(t, logs.String(), token) + assert.Contains(t, logs.String(), "***") +} + +func TestSlackSource_Chunks_ProviderErrorsAreRedactedBeforeLogging(t *testing.T) { + const token = "xoxb-sensitive-log-canary" + tests := []struct { + name string + mock *mockSlackClient + }{ + { + name: "channel list", + mock: &mockSlackClient{listErr: fmt.Errorf("SDK echoed Bearer %s", token)}, + }, + { + name: "history", + mock: &mockSlackClient{ + channels: []slack.Channel{{GroupConversation: slack.GroupConversation{Conversation: slack.Conversation{ID: "C1"}, Name: "general"}}}, + historyErr: fmt.Errorf("SDK echoed %s", token), + }, + }, + { + name: "rate retry exhaustion", + mock: &mockSlackClient{ + channels: []slack.Channel{{GroupConversation: slack.GroupConversation{Conversation: slack.Conversation{ID: "C1"}, Name: "general"}}}, + historyErr: fmt.Errorf("SDK echoed %s: %w", token, &slack.RateLimitedError{RetryAfter: time.Millisecond}), + }, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var logs bytes.Buffer + previous := slog.Default() + slog.SetDefault(slog.New(slog.NewTextHandler(&logs, nil))) + t.Cleanup(func() { slog.SetDefault(previous) }) + + s := New(token, WithRateLimit(1000)) + s.client = tc.mock + for range s.Chunks(context.Background()) { + } + require.Error(t, s.Err()) + assert.NotContains(t, logs.String(), token) + assert.NotContains(t, s.Err().Error(), token) + assert.Contains(t, logs.String(), "***") + assert.Contains(t, s.Err().Error(), "***") + }) + } +} + +func TestSlackSource_CaptureErrAlwaysFlattensUnwrapChain(t *testing.T) { + secretCause := errors.New("xoxb-hidden-in-unwrap") + s := New("xoxb-hidden-in-unwrap") + s.captureErr(hiddenSlackError{cause: secretCause}) + require.Error(t, s.Err()) + assert.Equal(t, "outer SDK failure", s.Err().Error()) + assert.False(t, errors.Is(s.Err(), secretCause)) +} + +func TestSlackSource_Chunks_IncludeFilesUsesBackpressureBeforeSecondDownload(t *testing.T) { + const ( + firstURL = "https://files.slack.com/files-pri/T1-F1/first" + secondURL = "https://files.slack.com/files-pri/T1-F2/second" + ) + started := make(chan string, 2) + mock := &mockSlackClient{ + channels: []slack.Channel{{GroupConversation: slack.GroupConversation{Conversation: slack.Conversation{ID: "C1"}, Name: "general"}}}, + messages: map[string][]slack.Message{"C1": {{Msg: slack.Msg{Files: []slack.File{{ID: "F1"}, {ID: "F2"}}}}}}, + files: map[string]slack.File{ + "F1": {ID: "F1", Name: "first.env", Mimetype: "text/plain", URLPrivateDownload: firstURL}, + "F2": {ID: "F2", Name: "second.env", Mimetype: "text/plain", URLPrivateDownload: secondURL}, + }, + fileBodies: map[string][]byte{firstURL: []byte("KEY=first"), secondURL: []byte("KEY=second")}, + downloadStarted: started, + } + s := New("xoxb-test-token", WithIncludeFiles(true), WithBufferSize(100), WithRateLimit(1000)) + s.client = mock + ctx, cancel := context.WithCancel(context.Background()) + chunks := s.Chunks(ctx) + assert.Zero(t, cap(chunks), "attachment mode must transfer engine backpressure directly") + + select { + case got := <-started: + assert.Equal(t, firstURL, got) + case <-time.After(time.Second): + t.Fatal("first attachment download did not start") + } + select { + case got := <-started: + t.Fatalf("second attachment downloaded before the first chunk was consumed: %s", got) + case <-time.After(50 * time.Millisecond): + } + cancel() + for range chunks { + } +} + +func TestSlackSource_DefaultMethodRateLimitsAreIndependent(t *testing.T) { + s := New("xoxb-test-token") + assert.Equal(t, DefaultRateLimit, s.historyRateLimit) + assert.Equal(t, defaultListRateLimit, s.listRateLimit) + assert.Equal(t, defaultFileRateLimit, s.fileInfoRateLimit) + assert.Equal(t, defaultFileRateLimit, s.fileDownloadRateLimit) + assert.NotEqual(t, s.historyRateLimit, s.listRateLimit) + assert.NotEqual(t, s.historyRateLimit, s.fileInfoRateLimit) + + WithRateLimit(7)(s) + assert.Equal(t, 7.0, s.historyRateLimit) + assert.Equal(t, 7.0, s.listRateLimit) + assert.Equal(t, 7.0, s.fileInfoRateLimit) + assert.Equal(t, 7.0, s.fileDownloadRateLimit) +} + +func TestSlackSource_DefaultListAndHistoryFirstRequestsDoNotBlockEachOther(t *testing.T) { + mock := &mockSlackClient{ + channels: []slack.Channel{{GroupConversation: slack.GroupConversation{Conversation: slack.Conversation{ID: "C1"}, Name: "general"}}}, + messages: map[string][]slack.Message{"C1": nil}, + } + s := New("xoxb-test-token") + s.client = mock + done := make(chan struct{}) + go func() { + for range s.Chunks(context.Background()) { + } + close(done) + }() + select { + case <-done: + case <-time.After(2 * time.Second): + t.Fatal("method-scoped first requests blocked one another") + } + assert.Equal(t, 1, mock.listCalls) + assert.Equal(t, 1, mock.historyCalls) +} + +func TestSlackAttachmentTextClassification(t *testing.T) { + assert.True(t, isTextLikeSlackFile(slack.File{Mimetype: "application/json; charset=utf-8"})) + assert.True(t, isTextLikeSlackFile(slack.File{Mimetype: "text/plain; charset=UTF-8"})) + assert.False(t, isTextLikeSlackFile(slack.File{Mimetype: "text/plain; broken"})) + assert.False(t, isTextLikeSlackFile(slack.File{Mimetype: "application/octet-stream"})) + + assert.True(t, isProbablyText([]byte("{\"secret\":\"value\"}\n"))) + assert.True(t, isProbablyText([]byte("anahtar=değer\n"))) + assert.False(t, isProbablyText([]byte{0xff, 0xd8, 0xff, 0xe0, 1, 2, 3})) + assert.False(t, isProbablyText([]byte{1, 2, 3, 4, 5})) + assert.False(t, isProbablyText([]byte("text\x00binary"))) +} + +func TestSlackSource_Chunks_RejectsSpoofedOrMissingMIMEBinaryContent(t *testing.T) { + const ( + spoofedURL = "https://files.slack.com/files-pri/T1-F1/spoofed" + missingURL = "https://files.slack.com/files-pri/T1-F2/missing" + ) + mock := &mockSlackClient{ + channels: []slack.Channel{{GroupConversation: slack.GroupConversation{Conversation: slack.Conversation{ID: "C1"}, Name: "general"}}}, + messages: map[string][]slack.Message{"C1": {{Msg: slack.Msg{Files: []slack.File{{ID: "F1"}, {ID: "F2"}}}}}}, + files: map[string]slack.File{ + "F1": {ID: "F1", Name: "image.txt", Mimetype: "text/plain", URLPrivateDownload: spoofedURL}, + "F2": {ID: "F2", Name: "archive", Mimetype: "", URLPrivateDownload: missingURL}, + }, + fileBodies: map[string][]byte{ + spoofedURL: {0xff, 0xd8, 0xff, 0xe0, 1, 2, 3}, + missingURL: {1, 2, 3, 4, 5}, + }, + } + s := New("xoxb-test-token", WithIncludeFiles(true), WithRateLimit(1000)) + s.client = mock + var chunks []source.Chunk + for chunk := range s.Chunks(context.Background()) { + chunks = append(chunks, chunk) + } + assert.Empty(t, chunks) + assert.Equal(t, 2, mock.downloadCalls) +} + +func TestBoundedFileBuffer_MaxAndMaxPlusOne(t *testing.T) { + exact := &boundedFileBuffer{remaining: 3} + n, err := exact.Write([]byte("abc")) + require.NoError(t, err) + assert.Equal(t, 3, n) + assert.Equal(t, "abc", exact.String()) + + over := &boundedFileBuffer{remaining: 3} + n, err = over.Write([]byte("abcd")) + assert.ErrorIs(t, err, errSlackFileTooLarge) + assert.Equal(t, 3, n) + assert.Equal(t, "abc", over.String()) +} + +func TestValidateSlackDownloadURL_RejectsCredentialAndNonSlackDestinations(t *testing.T) { + assert.NoError(t, validateSlackDownloadURL("https://files.slack.com/files-pri/T1-F1/file")) + assert.NoError(t, validateSlackDownloadURL("https://files.slack.com:443/files-pri/T1-F1/file")) + for _, raw := range []string{ + "http://files.slack.com/file", + "https://token@files.slack.com/file", + "https://files.slack.com:8443/file", + "https://downloads.slack.com/file", + "https://files.slack.com.attacker.example/file", + "https://127.0.0.1/file", + } { + assert.Error(t, validateSlackDownloadURL(raw), raw) + } +} + +func TestRejectSlackRedirect_PreventsCredentialForwarding(t *testing.T) { + redirectTargetCalls := 0 + target := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { + redirectTargetCalls++ + })) + defer target.Close() + origin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, target.URL, http.StatusFound) + })) + defer origin.Close() + + request, err := http.NewRequestWithContext(t.Context(), http.MethodGet, origin.URL, nil) + require.NoError(t, err) + response, err := newSlackHTTPClient().Do(request) + require.NoError(t, err) + require.NotNil(t, response) + defer response.Body.Close() + assert.Equal(t, http.StatusFound, response.StatusCode) + assert.Zero(t, redirectTargetCalls, "the authenticated Slack HTTP client must never follow a redirect") +} + +func TestSafeSlackPathSegment_RejectsTraversalAndControls(t *testing.T) { + assert.Equal(t, "config.env", safeSlackPathSegment("../config.env", "attachment")) + assert.Equal(t, "attachment", safeSlackPathSegment("..", "attachment")) + assert.Equal(t, "bad_name.txt", safeSlackPathSegment("bad\nname.txt", "attachment")) + assert.Equal(t, "秘密.env", safeSlackPathSegment("秘密.env", "attachment")) } func TestSlackSource_Chunks_ContextCancellation_Stops(t *testing.T) { @@ -535,6 +963,30 @@ func TestSlackSource_ListChannels_RateLimitedBeyondMaxRetries_ReturnsError(t *te assert.Equal(t, maxRateLimitRetries+1, mock.listCalls) } +func TestWaitRetryAfter_RejectsUnboundedProviderDelay(t *testing.T) { + start := time.Now() + err := waitRetryAfter(context.Background(), maximumRetryAfterWait+time.Second) + require.Error(t, err) + assert.Less(t, time.Since(start), 100*time.Millisecond) + assert.NotContains(t, err.Error(), (maximumRetryAfterWait + time.Second).String()) +} + +func TestSlackSource_Chunks_UnboundedRetryAfter_MarksScanFailed(t *testing.T) { + mock := &mockSlackClient{ + channels: []slack.Channel{{GroupConversation: slack.GroupConversation{Conversation: slack.Conversation{ID: "C001"}, Name: "general"}}}, + historyRateLimitedCalls: 1, + rateLimitRetryAfter: maximumRetryAfterWait + time.Second, + } + s := New("xoxb-test-token", WithRateLimit(1000)) + s.client = mock + + for range s.Chunks(context.Background()) { + } + require.Error(t, s.Err()) + assert.Contains(t, s.Err().Error(), "retry wait") + assert.Equal(t, 1, mock.historyCalls) +} + func TestSlackSource_ProcessChannel_RateLimited_RetriesAndEmits(t *testing.T) { mock := &mockSlackClient{ messages: map[string][]slack.Message{ @@ -551,7 +1003,7 @@ func TestSlackSource_ProcessChannel_RateLimited_RetriesAndEmits(t *testing.T) { limiter := rate.NewLimiter(rate.Inf, 1) channel := slack.Channel{GroupConversation: slack.GroupConversation{Conversation: slack.Conversation{ID: "C001"}, Name: "general"}} - s.processChannel(context.Background(), ch, limiter, channel) + s.processChannel(context.Background(), ch, limiter, limiter, limiter, channel) close(ch) var texts []string @@ -583,7 +1035,7 @@ func TestSlackSource_ProcessChannel_RateLimitedBeyondMaxRetries_GivesUpGracefull // Must not panic and must not hang; give it a bounded time budget. done := make(chan struct{}) go func() { - s.processChannel(context.Background(), ch, limiter, channel) + s.processChannel(context.Background(), ch, limiter, limiter, limiter, channel) close(ch) close(done) }() @@ -620,7 +1072,7 @@ func TestSlackSource_Chunks_ListError_ReturnsNoChunksWithoutPanic(t *testing.T) assert.Equal(t, 0, count) } -func TestSlackSource_Chunks_HistoryError_DegradesGracefullyWithoutPanic(t *testing.T) { +func TestSlackSource_Chunks_HistoryError_IsReportedAndStops(t *testing.T) { mock := &mockSlackClient{ channels: []slack.Channel{ {GroupConversation: slack.GroupConversation{Conversation: slack.Conversation{ID: "C001"}, Name: "general"}}, @@ -639,9 +1091,9 @@ func TestSlackSource_Chunks_HistoryError_DegradesGracefullyWithoutPanic(t *testi } assert.Equal(t, 0, count) - // Both channels must have been attempted; a history error on one - // channel must not abort the scan of subsequent channels. - assert.Equal(t, 2, mock.historyCalls) + require.Error(t, s.Err()) + assert.Contains(t, s.Err().Error(), "conversation history") + assert.Equal(t, 1, mock.historyCalls, "a terminal source error must stop partial scanning") } // --- Multi-page cursor pagination tests --- @@ -695,7 +1147,7 @@ func TestSlackSource_ProcessChannel_MultiPageCursorPagination_EmitsAllPages(t *t limiter := rate.NewLimiter(rate.Inf, 1) channel := slack.Channel{GroupConversation: slack.GroupConversation{Conversation: slack.Conversation{ID: "C001"}, Name: "general"}} - s.processChannel(context.Background(), ch, limiter, channel) + s.processChannel(context.Background(), ch, limiter, limiter, limiter, channel) close(ch) var texts []string @@ -707,6 +1159,37 @@ func TestSlackSource_ProcessChannel_MultiPageCursorPagination_EmitsAllPages(t *t assert.Equal(t, 2, mock.historyCalls) } +func TestSlackSource_StalledPaginationFailsInsteadOfLooping(t *testing.T) { + t.Run("channel list repeated cursor", func(t *testing.T) { + mock := &mockSlackClient{listPages: map[string]mockChannelPage{ + "": {nextCursor: "repeat"}, + "repeat": {nextCursor: "repeat"}, + }} + s := New("xoxb-test-token") + s.client = mock + _, err := s.listChannels(context.Background(), rate.NewLimiter(rate.Inf, 1)) + require.Error(t, err) + assert.Contains(t, err.Error(), "stalled cursor") + assert.Equal(t, 2, mock.listCalls) + }) + + t.Run("history empty cursor with has more", func(t *testing.T) { + mock := &mockSlackClient{ + channels: []slack.Channel{{GroupConversation: slack.GroupConversation{Conversation: slack.Conversation{ID: "C001"}, Name: "general"}}}, + historyPages: map[string]mockHistoryPage{ + "C001|": {hasMore: true, nextCursor: ""}, + }, + } + s := New("xoxb-test-token", WithRateLimit(1000)) + s.client = mock + for range s.Chunks(context.Background()) { + } + require.Error(t, s.Err()) + assert.Contains(t, s.Err().Error(), "stalled cursor") + assert.Equal(t, 1, mock.historyCalls) + }) +} + func TestSlackSource_Chunks_MultiPageChannelsAndHistory_EmitsAllAcrossPages(t *testing.T) { mock := &mockSlackClient{ listPages: map[string]mockChannelPage{ diff --git a/internal/source/source.go b/internal/source/source.go index 372ce3f..7717e5b 100644 --- a/internal/source/source.go +++ b/internal/source/source.go @@ -39,8 +39,10 @@ type Source interface { // Err before the channel closes is a data race. Err() error - // Validate checks that the source is accessible and valid. - Validate() error + // Validate checks that the source is accessible and valid. Implementations + // must honor cancellation and deadlines for every network or blocking + // operation performed during validation. + Validate(ctx context.Context) error } // Chunk is the smallest unit of data to be scanned. diff --git a/internal/verifier/airtable/airtable_shared_test.go b/internal/verifier/airtable/airtable_shared_test.go new file mode 100644 index 0000000..ae3d53d --- /dev/null +++ b/internal/verifier/airtable/airtable_shared_test.go @@ -0,0 +1,16 @@ +package airtable + +import ( + "net/http" + "testing" + + "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/internal/verifier" + "github.com/HodeTech/leakwatch/internal/verifier/internal/vtest" +) + +func TestVerify_SharedSafetySuite(t *testing.T) { + vtest.Run(t, vtest.Case{Name: "airtable", New: func(url string, client *http.Client) verifier.Verifier { + return &Verifier{apiURL: url, httpClient: client} + }, Raw: detector.RawFinding{DetectorID: detectorID, Raw: []byte("patSyntheticFixture.TokenValue")}}) +} diff --git a/internal/verifier/auth0/auth0_shared_test.go b/internal/verifier/auth0/auth0_shared_test.go new file mode 100644 index 0000000..a79634f --- /dev/null +++ b/internal/verifier/auth0/auth0_shared_test.go @@ -0,0 +1,16 @@ +package auth0 + +import ( + "net/http" + "testing" + + "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/internal/verifier" + "github.com/HodeTech/leakwatch/internal/verifier/internal/vtest" +) + +func TestVerify_SharedSafetySuite(t *testing.T) { + vtest.Run(t, vtest.Case{Name: "auth0", New: func(url string, client *http.Client) verifier.Verifier { + return &Verifier{apiURL: url, httpClient: client} + }, Raw: detector.RawFinding{DetectorID: detectorID, Raw: []byte("aaaaaaaaaaaaaaaa.bbbbbbbbbbbbbbbb.cccccccc")}}) +} diff --git a/internal/verifier/auth0/auth0_verifier.go b/internal/verifier/auth0/auth0_verifier.go index 2bea998..deaf86c 100644 --- a/internal/verifier/auth0/auth0_verifier.go +++ b/internal/verifier/auth0/auth0_verifier.go @@ -1,15 +1,16 @@ // Package auth0 provides a verifier for Auth0 Management API tokens. // It uses the Auth0 Management API GET /api/v2/ endpoint with Bearer auth // to check token validity. The Auth0 Management API is tenant-scoped, so the -// target host is derived from the token's own iss claim rather than a fixed host. +// target host must be explicitly trusted by the operator. Repository content +// and unverified JWT claims never select the request destination. package auth0 import ( "context" - "encoding/base64" "encoding/json" + "fmt" + "io" "net/http" - "strings" "github.com/HodeTech/leakwatch/internal/detector" "github.com/HodeTech/leakwatch/internal/verifier" @@ -19,6 +20,8 @@ import ( const detectorID = "auth0-management-token" +const clientsProbePath = "/api/v2/clients?per_page=1&include_fields=true&fields=client_id" + // Verifier checks whether an Auth0 Management API token is active by calling // the Auth0 Management API. It NEVER logs or persists raw token values. type Verifier struct { @@ -28,6 +31,22 @@ type Verifier struct { httpClient *http.Client } +// NewForTrustedInstance constructs an Auth0 verifier for an origin explicitly +// supplied by the operator. It must never receive an issuer decoded from an +// unverified token or a URL extracted from scanned repository content. +func NewForTrustedInstance(instanceURL string) (*Verifier, error) { + normalized, err := verifier.NormalizeTrustedHTTPSOrigin(instanceURL) + if err != nil { + return nil, err + } + return &Verifier{apiURL: normalized}, nil +} + +// WithTrustedInstance implements verifier.TrustedInstanceConfigurer. +func (*Verifier) WithTrustedInstance(instanceURL string) (verifier.Verifier, error) { + return NewForTrustedInstance(instanceURL) +} + func init() { verifier.Register(&Verifier{}) } @@ -39,11 +58,8 @@ func (v *Verifier) Type() string { // Verify checks if the detected Auth0 Management API token is valid/active. // -// An Auth0 Management API token is a JWT that is only valid against its own -// tenant's domain. The tenant host is taken from the token's iss claim (when -// apiURL is not overridden for testing). A non-JWT input, or a JWT without an -// iss claim, is indeterminate: the token cannot be routed to a tenant, so the -// result is StatusUnverified rather than a false "invalid". +// Auth0 Management API tokens are tenant-scoped. Without an operator-trusted +// tenant origin the result is indeterminate and no request is sent. func (v *Verifier) Verify(ctx context.Context, raw detector.RawFinding) finding.VerificationResult { token := string(raw.Raw) if token == "" { @@ -55,53 +71,35 @@ func (v *Verifier) Verify(ctx context.Context, raw detector.RawFinding) finding. apiURL := v.apiURL if apiURL == "" { - issuer, ok := issuerFromJWT(token) - if !ok { - return finding.VerificationResult{ - Status: finding.StatusUnverified, - Message: "Auth0 tenant domain could not be determined from token", - } + return finding.VerificationResult{ + Status: finding.StatusUnverified, + Message: "trusted Auth0 tenant origin is not configured", } - apiURL = strings.TrimSuffix(issuer, "/") } return httpx.VerifyToken(ctx, v.httpClient, token, httpx.TokenSpec{ Name: "auth0", Request: httpx.Request{ - URL: apiURL + "/api/v2/", + URL: apiURL + clientsProbePath, Header: map[string]string{"Authorization": "Bearer " + token}, }, - ActiveMessage: "Auth0 management token is active", - InactiveMessage: "Auth0 management token is invalid or expired", + ActiveMessage: "Auth0 management token is active", + InactiveMessage: "Auth0 management token is invalid or expired", + Decode: decodeClients, + RequireCompleteBody: true, + RequireJSONContentType: true, }) } -// issuerFromJWT decodes the unverified payload segment of a JWT and returns its -// iss claim, which for an Auth0 Management token is the tenant domain URL -// (for example "https://acme.eu.auth0.com/"). The signature is intentionally -// NOT validated: the claim is used only to route the verification request to -// the correct tenant host, and validity is decided by the live API call. It -// returns ok=false for a non-JWT input or a missing/empty iss claim. -func issuerFromJWT(token string) (issuer string, ok bool) { - parts := strings.Split(token, ".") - if len(parts) != 3 { - return "", false - } - - // JWT segments use base64url without padding, but tolerate padded input. - payload, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(parts[1], "=")) - if err != nil { - return "", false - } - - var claims struct { - Iss string `json:"iss"` +func decodeClients(body io.Reader) (map[string]string, string, error) { + var clients []struct { + ClientID string `json:"client_id"` } - if err := json.Unmarshal(payload, &claims); err != nil { - return "", false + if err := json.NewDecoder(body).Decode(&clients); err != nil { + return nil, "", err } - if claims.Iss == "" { - return "", false + if clients == nil { + return nil, "", fmt.Errorf("Auth0 clients response is not an array") } - return claims.Iss, true + return nil, "", nil } diff --git a/internal/verifier/auth0/auth0_verifier_test.go b/internal/verifier/auth0/auth0_verifier_test.go index ada5e3a..8b98141 100644 --- a/internal/verifier/auth0/auth0_verifier_test.go +++ b/internal/verifier/auth0/auth0_verifier_test.go @@ -25,12 +25,13 @@ func makeJWT(iss string) string { func TestVerify_ValidToken_ReturnsActive(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/api/v2/", r.URL.Path) + assert.Equal(t, "/api/v2/clients", r.URL.Path) + assert.Equal(t, "1", r.URL.Query().Get("per_page")) assert.Contains(t, r.Header.Get("Authorization"), "Bearer ") w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(`{}`)) + _, _ = w.Write([]byte(`[]`)) })) defer server.Close() @@ -53,6 +54,7 @@ func TestVerify_ValidToken_ReturnsActive(t *testing.T) { func TestVerify_InvalidToken_ReturnsInactive(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"statusCode":401,"error":"Unauthorized","message":"Invalid token"}`)) })) @@ -118,10 +120,7 @@ func TestVerify_EmptyToken_ReturnsUnverified(t *testing.T) { assert.Equal(t, "empty token", result.Message) } -func TestVerify_NonJWTToken_ReturnsUnverified(t *testing.T) { - // No apiURL override: the verifier must derive the tenant from the token - // itself. A non-JWT token cannot be routed, so the result is indeterminate - // (StatusUnverified) — never a false "invalid". +func TestVerify_MissingTrustedOrigin_ReturnsUnverified(t *testing.T) { v := &Verifier{} raw := detector.RawFinding{ @@ -133,21 +132,17 @@ func TestVerify_NonJWTToken_ReturnsUnverified(t *testing.T) { result := v.Verify(context.Background(), raw) assert.Equal(t, finding.StatusUnverified, result.Status) - assert.Contains(t, result.Message, "tenant domain") + assert.Contains(t, result.Message, "trusted Auth0 tenant origin") } -func TestVerify_TenantHostDerivedFromIssClaim(t *testing.T) { - var gotHost string +func TestVerify_UntrustedIssuerClaimCannotSelectDestination(t *testing.T) { + called := false server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - gotHost = r.Host - assert.Equal(t, "/api/v2/", r.URL.Path) - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(`{}`)) + called = true + t.Fatalf("untrusted JWT issuer triggered request to %s", r.URL) })) defer server.Close() - // The iss claim points the verifier at the test server; apiURL is left - // unset so the iss-decoding path (not the test override) is exercised. v := &Verifier{httpClient: server.Client()} raw := detector.RawFinding{ @@ -158,50 +153,20 @@ func TestVerify_TenantHostDerivedFromIssClaim(t *testing.T) { result := v.Verify(context.Background(), raw) - require.Equal(t, finding.StatusVerifiedActive, result.Status) - assert.NotEmpty(t, gotHost) + require.Equal(t, finding.StatusUnverified, result.Status) + assert.False(t, called) } -func TestIssuerFromJWT(t *testing.T) { - tests := []struct { - name string - token string - wantIss string - wantOK bool - }{ - { - name: "valid JWT with iss", - token: makeJWT("https://acme.eu.auth0.com/"), - wantIss: "https://acme.eu.auth0.com/", - wantOK: true, - }, - { - name: "not a JWT (single segment)", - token: "plaintextsecret", - wantOK: false, - }, - { - name: "wrong segment count", - token: "a.b.c.d", - wantOK: false, - }, - { - name: "JWT without iss claim", - token: makeJWT(""), - wantOK: false, - }, - { - name: "invalid base64 payload", - token: "aaa.!!!not-base64!!!.ccc", - wantOK: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - iss, ok := issuerFromJWT(tt.token) - assert.Equal(t, tt.wantOK, ok) - assert.Equal(t, tt.wantIss, iss) - }) +func TestNewForTrustedInstance_ValidatesOrigin(t *testing.T) { + configured, err := NewForTrustedInstance("https://fixture.eu.auth0.com/") + require.NoError(t, err) + assert.Equal(t, "https://fixture.eu.auth0.com", configured.apiURL) + + for _, origin := range []string{ + "http://fixture.auth0.com", "https://127.0.0.1", "https://localhost", + "https://user:pass@fixture.auth0.com", "https://fixture.auth0.com/path", + } { + _, err := NewForTrustedInstance(origin) + assert.Error(t, err, origin) } } diff --git a/internal/verifier/aws/aws_verifier.go b/internal/verifier/aws/aws_verifier.go index f586aaa..3dcbc57 100644 --- a/internal/verifier/aws/aws_verifier.go +++ b/internal/verifier/aws/aws_verifier.go @@ -33,6 +33,8 @@ type Verifier struct { client stsClient } +var _ verifier.RequestGatedVerifier = (*Verifier)(nil) + func init() { verifier.Register(&Verifier{}) } @@ -46,6 +48,27 @@ func (v *Verifier) Type() string { // Raw contains the Access Key ID and RawV2 contains the Secret Access Key. // Both are required for verification. func (v *Verifier) Verify(ctx context.Context, raw detector.RawFinding) finding.VerificationResult { + return v.verify(ctx, raw, nil) +} + +// VerificationRequestBudget declares the single STS probe this verifier can +// issue for a complete key pair. +func (v *Verifier) VerificationRequestBudget() int { return 1 } + +// VerifyWithRequestGate admits the STS request at its actual SDK send point. +func (v *Verifier) VerifyWithRequestGate( + ctx context.Context, + raw detector.RawFinding, + gate verifier.RequestGate, +) finding.VerificationResult { + return v.verify(ctx, raw, gate) +} + +func (v *Verifier) verify( + ctx context.Context, + raw detector.RawFinding, + gate verifier.RequestGate, +) finding.VerificationResult { if len(raw.RawV2) == 0 { slog.DebugContext(ctx, "aws verifier: secret access key not found, skipping verification") return finding.VerificationResult{ @@ -65,6 +88,11 @@ func (v *Verifier) Verify(ctx context.Context, raw detector.RawFinding) finding. ), }) } + if gate != nil { + if rejection := gate(); rejection != nil { + return *rejection + } + } output, err := client.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{}) if err != nil { diff --git a/internal/verifier/aws/aws_verifier_test.go b/internal/verifier/aws/aws_verifier_test.go index 4056fb9..3e5dd7e 100644 --- a/internal/verifier/aws/aws_verifier_test.go +++ b/internal/verifier/aws/aws_verifier_test.go @@ -13,16 +13,34 @@ import ( "github.com/stretchr/testify/require" "github.com/HodeTech/leakwatch/internal/detector" + awsdetector "github.com/HodeTech/leakwatch/internal/detector/aws" + "github.com/HodeTech/leakwatch/internal/detector/testutil" "github.com/HodeTech/leakwatch/pkg/finding" ) +func TestVerify_RealDetectorOutput_ReachesSTSContract(t *testing.T) { + fixture := testutil.RegisteredDetectorFixtures()[detectorID] + findings := testutil.ScanViaMatcher(&awsdetector.AccessKeyID{}, fixture.Input) + require.Len(t, findings, 1) + require.NotEmpty(t, findings[0].RawV2) + + v := &Verifier{client: &mockSTSClient{output: &sts.GetCallerIdentityOutput{ + Account: aws.String("123456789012"), Arn: aws.String("arn:aws:iam::123456789012:user/fixture"), + UserId: aws.String("AIDAFIXTURE"), + }}} + result := v.Verify(t.Context(), findings[0]) + require.Equal(t, finding.StatusVerifiedActive, result.Status) +} + // mockSTSClient implements stsClient for testing. type mockSTSClient struct { output *sts.GetCallerIdentityOutput err error + calls int } func (m *mockSTSClient) GetCallerIdentity(_ context.Context, _ *sts.GetCallerIdentityInput, _ ...func(*sts.Options)) (*sts.GetCallerIdentityOutput, error) { + m.calls++ return m.output, m.err } @@ -42,6 +60,47 @@ func TestVerify_NoSecretKey_ReturnsUnverified(t *testing.T) { assert.Equal(t, "secret access key not found", result.Message) } +func TestVerifyWithRequestGate_AdmitsOnlyActualSTSCall(t *testing.T) { + t.Run("missing companion key consumes no admission", func(t *testing.T) { + gateCalls := 0 + result := (&Verifier{}).VerifyWithRequestGate(context.Background(), detector.RawFinding{ + Raw: []byte("AKIAIOSFODNN7EXAMPLE"), + }, func() *finding.VerificationResult { + gateCalls++ + return nil + }) + assert.Equal(t, finding.StatusUnverified, result.Status) + assert.Zero(t, gateCalls) + }) + + t.Run("complete pair gets one admission", func(t *testing.T) { + mock := &mockSTSClient{output: &sts.GetCallerIdentityOutput{}} + v := &Verifier{client: mock} + gateCalls := 0 + result := v.VerifyWithRequestGate(context.Background(), detector.RawFinding{ + Raw: []byte("AKIAIOSFODNN7EXAMPLE"), RawV2: []byte("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"), + }, func() *finding.VerificationResult { + gateCalls++ + return nil + }) + assert.Equal(t, finding.StatusVerifiedActive, result.Status) + assert.Equal(t, 1, gateCalls) + assert.Equal(t, 1, mock.calls) + }) + + t.Run("rejection prevents SDK call", func(t *testing.T) { + mock := &mockSTSClient{output: &sts.GetCallerIdentityOutput{}} + v := &Verifier{client: mock} + result := v.VerifyWithRequestGate(context.Background(), detector.RawFinding{ + Raw: []byte("AKIAIOSFODNN7EXAMPLE"), RawV2: []byte("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"), + }, func() *finding.VerificationResult { + return &finding.VerificationResult{Status: finding.StatusVerifyError, Message: "admission rejected"} + }) + assert.Equal(t, "admission rejected", result.Message) + assert.Zero(t, mock.calls) + }) +} + func TestVerify_Type_ReturnsCorrectID(t *testing.T) { v := &Verifier{} assert.Equal(t, "aws-access-key-id", v.Type()) diff --git a/internal/verifier/bitbucket/bitbucket_shared_test.go b/internal/verifier/bitbucket/bitbucket_shared_test.go new file mode 100644 index 0000000..5841211 --- /dev/null +++ b/internal/verifier/bitbucket/bitbucket_shared_test.go @@ -0,0 +1,16 @@ +package bitbucket + +import ( + "net/http" + "testing" + + "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/internal/verifier" + "github.com/HodeTech/leakwatch/internal/verifier/internal/vtest" +) + +func TestVerify_SharedSafetySuite(t *testing.T) { + vtest.Run(t, vtest.Case{Name: "bitbucket", New: func(url string, client *http.Client) verifier.Verifier { + return &Verifier{apiURL: url, httpClient: client} + }, Raw: detector.RawFinding{DetectorID: detectorID, Raw: []byte("SyntheticAppPassword12"), ExtraData: map[string]string{"username": "fixture-user"}}}) +} diff --git a/internal/verifier/bitbucket/bitbucket_verifier_test.go b/internal/verifier/bitbucket/bitbucket_verifier_test.go index 95384d3..1b21f57 100644 --- a/internal/verifier/bitbucket/bitbucket_verifier_test.go +++ b/internal/verifier/bitbucket/bitbucket_verifier_test.go @@ -12,9 +12,31 @@ import ( "github.com/stretchr/testify/require" "github.com/HodeTech/leakwatch/internal/detector" + bitbucketdetector "github.com/HodeTech/leakwatch/internal/detector/bitbucket" + "github.com/HodeTech/leakwatch/internal/detector/testutil" "github.com/HodeTech/leakwatch/pkg/finding" ) +func TestVerify_RealDetectorOutput_UsesPairedUsername(t *testing.T) { + fixture := testutil.RegisteredDetectorFixtures()[detectorID] + findings := testutil.ScanViaMatcher(&bitbucketdetector.Detector{}, fixture.Input) + require.Len(t, findings, 1) + require.Equal(t, "fixture-user", findings[0].ExtraData["username"]) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + username, password, ok := r.BasicAuth() + require.True(t, ok) + assert.Equal(t, findings[0].ExtraData["username"], username) + assert.Equal(t, string(findings[0].Raw), password) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"display_name":"Fixture User"}`)) + })) + defer server.Close() + + result := (&Verifier{apiURL: server.URL, httpClient: server.Client()}).Verify(t.Context(), findings[0]) + require.Equal(t, finding.StatusVerifiedActive, result.Status) +} + func TestVerify_ValidPassword_ReturnsActive(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "/2.0/user", r.URL.Path) diff --git a/internal/verifier/cloudflare/cloudflare_shared_test.go b/internal/verifier/cloudflare/cloudflare_shared_test.go new file mode 100644 index 0000000..302a266 --- /dev/null +++ b/internal/verifier/cloudflare/cloudflare_shared_test.go @@ -0,0 +1,16 @@ +package cloudflare + +import ( + "net/http" + "testing" + + "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/internal/verifier" + "github.com/HodeTech/leakwatch/internal/verifier/internal/vtest" +) + +func TestVerify_SharedSafetySuite(t *testing.T) { + vtest.Run(t, vtest.Case{Name: "cloudflare", New: func(url string, client *http.Client) verifier.Verifier { + return &Verifier{apiURL: url, httpClient: client} + }, Raw: detector.RawFinding{DetectorID: detectorID, Raw: []byte("SyntheticCloudflareTokenFixture123456")}}) +} diff --git a/internal/verifier/configuration.go b/internal/verifier/configuration.go new file mode 100644 index 0000000..9c8b5a5 --- /dev/null +++ b/internal/verifier/configuration.go @@ -0,0 +1,73 @@ +package verifier + +import ( + "fmt" + "reflect" +) + +// TrustedInstanceConfigurer is implemented by verifiers whose live endpoint +// must be selected from explicit operator context rather than scanned content. +// Implementations must validate the supplied origin and return an independent +// verifier value; they must not mutate the process-global registry instance. +type TrustedInstanceConfigurer interface { + WithTrustedInstance(instanceURL string) (Verifier, error) +} + +// ConfigureTrustedInstance returns a per-run verifier slice in which detectorID +// is configured for an explicit operator-supplied instance. The input slice and +// the process-global registry objects are never mutated. +func ConfigureTrustedInstance(vs []Verifier, detectorID, instanceURL string) ([]Verifier, error) { + configured := append([]Verifier(nil), vs...) + if detectorID == "" { + return nil, fmt.Errorf("trusted instance verifier ID must not be empty") + } + + target := -1 + seen := make(map[string]struct{}, len(configured)) + for i, candidate := range configured { + if isNilVerifier(candidate) { + return nil, fmt.Errorf("verifier list contains nil entry at index %d", i) + } + id := candidate.Type() + if id == "" { + return nil, fmt.Errorf("verifier list contains empty type at index %d", i) + } + if _, duplicate := seen[id]; duplicate { + return nil, fmt.Errorf("verifier list contains duplicate type %q", id) + } + seen[id] = struct{}{} + if id == detectorID { + target = i + } + } + if target < 0 { + return nil, fmt.Errorf("verifier %q is not registered", detectorID) + } + + configurer, ok := configured[target].(TrustedInstanceConfigurer) + if !ok { + return nil, fmt.Errorf("verifier %q does not accept trusted instance configuration", detectorID) + } + replacement, err := configurer.WithTrustedInstance(instanceURL) + if err != nil { + return nil, err + } + if isNilVerifier(replacement) || replacement.Type() != detectorID { + return nil, fmt.Errorf("verifier %q returned an invalid configured replacement", detectorID) + } + configured[target] = replacement + return configured, nil +} + +func isNilVerifier(v Verifier) bool { + if v == nil { + return true + } + value := reflect.ValueOf(v) + switch value.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice: + return value.IsNil() + default: + return false + } +} diff --git a/internal/verifier/configuration_test.go b/internal/verifier/configuration_test.go new file mode 100644 index 0000000..03faa1f --- /dev/null +++ b/internal/verifier/configuration_test.go @@ -0,0 +1,77 @@ +package verifier + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/pkg/finding" +) + +type trustedInstanceMock struct { + id string + instance string + err error + replacement Verifier +} + +func (m *trustedInstanceMock) Type() string { return m.id } + +func (m *trustedInstanceMock) Verify(context.Context, detector.RawFinding) finding.VerificationResult { + return finding.VerificationResult{} +} + +func (m *trustedInstanceMock) WithTrustedInstance(instanceURL string) (Verifier, error) { + if m.err != nil { + return nil, m.err + } + if m.replacement != nil { + return m.replacement, nil + } + return &trustedInstanceMock{id: m.id, instance: instanceURL}, nil +} + +func TestConfigureTrustedInstance_ReturnsIndependentReplacement(t *testing.T) { + original := &trustedInstanceMock{id: "contextual"} + other := newMock("other") + input := []Verifier{original, other} + + got, err := ConfigureTrustedInstance(input, "contextual", "https://operator.example") + require.NoError(t, err) + require.Len(t, got, 2) + assert.Same(t, original, input[0]) + assert.Same(t, other, input[1]) + assert.NotSame(t, original, got[0]) + assert.Equal(t, "https://operator.example", got[0].(*trustedInstanceMock).instance) + assert.Same(t, other, got[1]) +} + +func TestConfigureTrustedInstance_RejectsInvalidContracts(t *testing.T) { + var typedNil *trustedInstanceMock + tests := []struct { + name string + vs []Verifier + id string + }{ + {name: "missing", vs: []Verifier{newMock("other")}, id: "missing"}, + {name: "not configurable", vs: []Verifier{newMock("plain")}, id: "plain"}, + {name: "configuration error", vs: []Verifier{&trustedInstanceMock{id: "bad", err: fmt.Errorf("invalid origin")}}, id: "bad"}, + {name: "nil entry", vs: []Verifier{nil}, id: "nil"}, + {name: "typed nil entry", vs: []Verifier{typedNil}, id: "nil"}, + {name: "duplicate type", vs: []Verifier{newMock("duplicate"), newMock("duplicate")}, id: "duplicate"}, + {name: "mismatched replacement", vs: []Verifier{&trustedInstanceMock{id: "contextual", replacement: newMock("other")}}, id: "contextual"}, + {name: "typed nil replacement", vs: []Verifier{&trustedInstanceMock{id: "contextual", replacement: typedNil}}, id: "contextual"}, + {name: "empty target ID", vs: []Verifier{newMock("other")}, id: ""}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, err := ConfigureTrustedInstance(tc.vs, tc.id, "https://operator.example") + require.Error(t, err) + assert.Nil(t, got) + }) + } +} diff --git a/internal/verifier/databricks/databricks_shared_test.go b/internal/verifier/databricks/databricks_shared_test.go new file mode 100644 index 0000000..a274e33 --- /dev/null +++ b/internal/verifier/databricks/databricks_shared_test.go @@ -0,0 +1,16 @@ +package databricks + +import ( + "net/http" + "testing" + + "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/internal/verifier" + "github.com/HodeTech/leakwatch/internal/verifier/internal/vtest" +) + +func TestVerify_SharedSafetySuite(t *testing.T) { + vtest.Run(t, vtest.Case{Name: "databricks", New: func(url string, client *http.Client) verifier.Verifier { + return &Verifier{apiURL: url, httpClient: client} + }, Raw: detector.RawFinding{DetectorID: detectorID, Raw: []byte("dapi0123456789abcdef0123456789abcdef")}}) +} diff --git a/internal/verifier/databricks/databricks_verifier_test.go b/internal/verifier/databricks/databricks_verifier_test.go index 7b11f33..052d258 100644 --- a/internal/verifier/databricks/databricks_verifier_test.go +++ b/internal/verifier/databricks/databricks_verifier_test.go @@ -2,17 +2,47 @@ package databricks import ( "context" + "io" "net/http" "net/http/httptest" + "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/HodeTech/leakwatch/internal/detector" + databricksdetector "github.com/HodeTech/leakwatch/internal/detector/databricks" + "github.com/HodeTech/leakwatch/internal/detector/testutil" "github.com/HodeTech/leakwatch/pkg/finding" ) +func TestVerify_RealDetectorOutput_UsesWorkspaceHost(t *testing.T) { + fixture := testutil.RegisteredDetectorFixtures()[detectorID] + findings := testutil.ScanViaMatcher(&databricksdetector.Detector{}, fixture.Input) + require.Len(t, findings, 1) + wantOrigin := findings[0].ExtraData["host"] + require.NotEmpty(t, wantOrigin) + + var gotURL string + client := &http.Client{Transport: databricksRoundTrip(func(r *http.Request) (*http.Response, error) { + gotURL = r.URL.String() + return &http.Response{ + StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(strings.NewReader(`{"userName":"fixture@example.com"}`)), Request: r, + }, nil + })} + result := (&Verifier{httpClient: client}).Verify(t.Context(), findings[0]) + require.Equal(t, finding.StatusVerifiedActive, result.Status) + assert.Equal(t, wantOrigin+"/api/2.0/preview/scim/v2/Me", gotURL) +} + +type databricksRoundTrip func(*http.Request) (*http.Response, error) + +func (f databricksRoundTrip) RoundTrip(request *http.Request) (*http.Response, error) { + return f(request) +} + func TestVerify_ValidToken_ReturnsActive(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "/api/2.0/preview/scim/v2/Me", r.URL.Path) diff --git a/internal/verifier/datadog/datadog_verifier.go b/internal/verifier/datadog/datadog_verifier.go index 7d498d0..617a669 100644 --- a/internal/verifier/datadog/datadog_verifier.go +++ b/internal/verifier/datadog/datadog_verifier.go @@ -5,8 +5,11 @@ package datadog import ( "context" "encoding/json" + "fmt" "io" "net/http" + "net/url" + "strings" "github.com/HodeTech/leakwatch/internal/detector" "github.com/HodeTech/leakwatch/internal/verifier" @@ -16,18 +19,46 @@ import ( const detectorID = "datadog-api-key" -// defaultAPIURL is the base URL for the Datadog API. -const defaultAPIURL = "https://api.datadoghq.com" - // Verifier checks whether a Datadog API key is active by calling the // Datadog validation API. It NEVER logs or persists raw key values. type Verifier struct { - // apiURL overrides the Datadog API base URL (for testing). + // apiURL is reserved for a validated, trusted Datadog site origin. The + // production registry value stays empty and each scan may install an + // independently configured copy; tests inject a local server. apiURL string // httpClient overrides the default HTTP client (for testing). httpClient *http.Client } +var trustedDatadogAPIHosts = map[string]struct{}{ + "api.datadoghq.com": {}, "api.us3.datadoghq.com": {}, "api.us5.datadoghq.com": {}, + "api.datadoghq.eu": {}, "api.ap1.datadoghq.com": {}, "api.ap2.datadoghq.com": {}, + "api.datad0g.com": {}, "api.ddog-gov.com": {}, "api.us2.ddog-gov.com": {}, +} + +// NewForTrustedInstance accepts only canonical official Datadog API site +// origins. Datadog keys are site-bound; arbitrary repository or custom hosts +// are never valid routing authorities. +func NewForTrustedInstance(instanceURL string) (*Verifier, error) { + normalized, err := verifier.NormalizeTrustedHTTPSOrigin(instanceURL) + if err != nil { + return nil, err + } + u, err := url.Parse(normalized) + if err != nil || u.Port() != "" { + return nil, fmt.Errorf("invalid Datadog API origin") + } + if _, ok := trustedDatadogAPIHosts[u.Hostname()]; !ok { + return nil, fmt.Errorf("invalid Datadog API origin: host is not an official Datadog site") + } + return &Verifier{apiURL: normalized}, nil +} + +// WithTrustedInstance implements verifier.TrustedInstanceConfigurer. +func (*Verifier) WithTrustedInstance(instanceURL string) (verifier.Verifier, error) { + return NewForTrustedInstance(instanceURL) +} + func init() { verifier.Register(&Verifier{}) } @@ -42,7 +73,16 @@ func (v *Verifier) Type() string { // 403 (not 401) for a rejected key. func (v *Verifier) Verify(ctx context.Context, raw detector.RawFinding) finding.VerificationResult { token := string(raw.Raw) - apiURL := httpx.BaseURL(v.apiURL, defaultAPIURL) + if token == "" { + return finding.VerificationResult{Status: finding.StatusUnverified, Message: "empty token"} + } + if v.apiURL == "" { + return finding.VerificationResult{ + Status: finding.StatusUnverified, + Message: "trusted Datadog site is not configured", + } + } + apiURL := v.apiURL return httpx.VerifyToken(ctx, v.httpClient, token, httpx.TokenSpec{ Name: "datadog", @@ -50,22 +90,45 @@ func (v *Verifier) Verify(ctx context.Context, raw detector.RawFinding) finding. URL: apiURL + "/api/v1/validate", Header: map[string]string{"DD-API-KEY": token}, }, - InactiveStatuses: []int{http.StatusForbidden}, - ActiveMessage: "Datadog API key is active", - InactiveMessage: "Datadog API key is invalid or revoked", - Decode: decodeValidate, + InactiveStatuses: []int{http.StatusForbidden}, + ActiveMessage: "Datadog API key is active", + InactiveMessage: "Datadog API key is invalid or revoked", + Decode: decodeValidate, + DecodeInactive: decodeInactiveResponse, + RequireCompleteBody: true, + RequireJSONContentType: true, }) } +func decodeInactiveResponse(body io.Reader) error { + var response struct { + Errors []string `json:"errors"` + } + decoder := json.NewDecoder(body) + if err := decoder.Decode(&response); err != nil { + return err + } + if len(response.Errors) != 1 || strings.TrimSpace(response.Errors[0]) != "Forbidden" { + return fmt.Errorf("unexpected Datadog authentication error") + } + if decoder.Decode(&struct{}{}) != io.EOF { + return fmt.Errorf("unexpected trailing Datadog response data") + } + return nil +} + // decodeValidate downgrades a 200 response to inactive when valid=false. func decodeValidate(body io.Reader) (map[string]string, string, error) { var resp struct { - Valid bool `json:"valid"` + Valid *bool `json:"valid"` } if err := json.NewDecoder(body).Decode(&resp); err != nil { return nil, "", err } - if !resp.Valid { + if resp.Valid == nil { + return nil, "", fmt.Errorf("missing Datadog valid field") + } + if !*resp.Valid { return nil, "Datadog API key is invalid", nil } return nil, "", nil diff --git a/internal/verifier/datadog/datadog_verifier_test.go b/internal/verifier/datadog/datadog_verifier_test.go index 8fade22..bffad8f 100644 --- a/internal/verifier/datadog/datadog_verifier_test.go +++ b/internal/verifier/datadog/datadog_verifier_test.go @@ -66,8 +66,36 @@ func TestVerify_InvalidKey_200False_ReturnsInactive(t *testing.T) { assert.Equal(t, "Datadog API key is invalid", result.Message) } +func TestVerify_MalformedSuccessResponse_ReturnsVerifyError(t *testing.T) { + tests := []struct { + name string + contentType string + body string + }{ + {name: "missing valid", contentType: "application/json", body: `{}`}, + {name: "null body", contentType: "application/json", body: `null`}, + {name: "wrong content type", contentType: "text/plain", body: `{"valid":true}`}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", tc.contentType) + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(tc.body)) + })) + defer server.Close() + + result := (&Verifier{apiURL: server.URL, httpClient: server.Client()}).Verify( + context.Background(), detector.RawFinding{Raw: []byte("synthetic-datadog-key")}, + ) + assert.Equal(t, finding.StatusVerifyError, result.Status) + }) + } +} + func TestVerify_InvalidKey_403_ReturnsInactive(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusForbidden) _, _ = w.Write([]byte(`{"errors":["Forbidden"]}`)) })) @@ -90,6 +118,25 @@ func TestVerify_InvalidKey_403_ReturnsInactive(t *testing.T) { assert.Equal(t, "Datadog API key is invalid or revoked", result.Message) } +func TestVerify_InactiveResponseMustBeDefinitiveJSON(t *testing.T) { + tests := []struct{ contentType, body string }{ + {contentType: "text/html", body: `WAF block`}, + {contentType: "application/json", body: `{"errors":["challenge"]}`}, + } + for _, tc := range tests { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", tc.contentType) + w.WriteHeader(http.StatusForbidden) + _, _ = w.Write([]byte(tc.body)) + })) + result := (&Verifier{apiURL: server.URL, httpClient: server.Client()}).Verify( + context.Background(), detector.RawFinding{Raw: []byte("synthetic-datadog-key")}, + ) + server.Close() + assert.Equal(t, finding.StatusVerifyError, result.Status) + } +} + func TestVerify_ServerError_ReturnsError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) @@ -133,3 +180,29 @@ func TestVerify_EmptyToken_ReturnsUnverified(t *testing.T) { assert.Equal(t, finding.StatusUnverified, result.Status) assert.Equal(t, "empty token", result.Message) } + +func TestVerify_WithoutTrustedSite_MakesNoRequest(t *testing.T) { + v := &Verifier{} + result := v.Verify(context.Background(), detector.RawFinding{Raw: []byte("synthetic-datadog-key")}) + + assert.Equal(t, finding.StatusUnverified, result.Status) + assert.Equal(t, "trusted Datadog site is not configured", result.Message) +} + +func TestNewForTrustedInstance_AcceptsOnlyOfficialDatadogSites(t *testing.T) { + for _, origin := range []string{ + "https://api.datadoghq.com", "https://api.datadoghq.eu", "https://api.ap2.datadoghq.com", + "https://api.ddog-gov.com", "https://api.us2.ddog-gov.com", + } { + configured, err := NewForTrustedInstance(origin) + require.NoError(t, err, origin) + assert.Equal(t, origin, configured.apiURL) + } + for _, origin := range []string{ + "https://datadoghq.com", "https://api.datadoghq.com.attacker.example", + "https://api.datadoghq.com:443", "http://api.datadoghq.com", + } { + _, err := NewForTrustedInstance(origin) + assert.Error(t, err, origin) + } +} diff --git a/internal/verifier/deepseek/deepseek_shared_test.go b/internal/verifier/deepseek/deepseek_shared_test.go new file mode 100644 index 0000000..ac85b2c --- /dev/null +++ b/internal/verifier/deepseek/deepseek_shared_test.go @@ -0,0 +1,16 @@ +package deepseek + +import ( + "net/http" + "testing" + + "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/internal/verifier" + "github.com/HodeTech/leakwatch/internal/verifier/internal/vtest" +) + +func TestVerify_SharedSafetySuite(t *testing.T) { + vtest.Run(t, vtest.Case{Name: "deepseek", New: func(url string, client *http.Client) verifier.Verifier { + return &Verifier{apiURL: url, httpClient: client} + }, Raw: detector.RawFinding{DetectorID: detectorID, Raw: []byte("sk-0123456789abcdef0123456789abcdef")}}) +} diff --git a/internal/verifier/digitalocean/digitalocean_shared_test.go b/internal/verifier/digitalocean/digitalocean_shared_test.go new file mode 100644 index 0000000..06bd44a --- /dev/null +++ b/internal/verifier/digitalocean/digitalocean_shared_test.go @@ -0,0 +1,16 @@ +package digitalocean + +import ( + "net/http" + "testing" + + "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/internal/verifier" + "github.com/HodeTech/leakwatch/internal/verifier/internal/vtest" +) + +func TestVerify_SharedSafetySuite(t *testing.T) { + vtest.Run(t, vtest.Case{Name: "digitalocean", New: func(url string, client *http.Client) verifier.Verifier { + return &Verifier{apiURL: url, httpClient: client} + }, Raw: detector.RawFinding{DetectorID: detectorID, Raw: []byte("dop_v1_0123456789abcdef0123456789abcdef")}}) +} diff --git a/internal/verifier/discord/discord_shared_test.go b/internal/verifier/discord/discord_shared_test.go new file mode 100644 index 0000000..f9259a4 --- /dev/null +++ b/internal/verifier/discord/discord_shared_test.go @@ -0,0 +1,16 @@ +package discord + +import ( + "net/http" + "testing" + + "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/internal/verifier" + "github.com/HodeTech/leakwatch/internal/verifier/internal/vtest" +) + +func TestVerify_SharedSafetySuite(t *testing.T) { + vtest.Run(t, vtest.Case{Name: "discord", New: func(url string, client *http.Client) verifier.Verifier { + return &Verifier{apiURL: url, httpClient: client} + }, Raw: detector.RawFinding{DetectorID: detectorID, Raw: []byte("MSyntheticDiscord.Fixtur.SyntheticCredentialValue")}}) +} diff --git a/internal/verifier/doppler/doppler_shared_test.go b/internal/verifier/doppler/doppler_shared_test.go new file mode 100644 index 0000000..696c5f9 --- /dev/null +++ b/internal/verifier/doppler/doppler_shared_test.go @@ -0,0 +1,16 @@ +package doppler + +import ( + "net/http" + "testing" + + "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/internal/verifier" + "github.com/HodeTech/leakwatch/internal/verifier/internal/vtest" +) + +func TestVerify_SharedSafetySuite(t *testing.T) { + vtest.Run(t, vtest.Case{Name: "doppler", New: func(url string, client *http.Client) verifier.Verifier { + return &Verifier{apiURL: url, httpClient: client} + }, Raw: detector.RawFinding{DetectorID: detectorID, Raw: []byte("dp.st.SyntheticDopplerCredentialFixture123456")}, SkipMalformed: true}) +} diff --git a/internal/verifier/engine.go b/internal/verifier/engine.go index a14d582..03d2772 100644 --- a/internal/verifier/engine.go +++ b/internal/verifier/engine.go @@ -11,10 +11,12 @@ import ( "golang.org/x/time/rate" "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/internal/verifier/internal/httpx" "github.com/HodeTech/leakwatch/pkg/finding" ) -// DefaultTimeout is the default per-request verification timeout. +// DefaultTimeout is the default timeout for one finding's complete verification +// operation, including any bounded provider-region fallback requests. const DefaultTimeout = 10 * time.Second // DefaultConcurrency is the default number of concurrent verification workers. @@ -23,12 +25,17 @@ const DefaultConcurrency = 4 // DefaultRateLimit is the default maximum verification requests per second. const DefaultRateLimit = 10.0 +// maxVerificationRequestBudget prevents a buggy verifier from reserving an +// unbounded number of limiter tokens for one finding. +const maxVerificationRequestBudget = 8 + // Config holds the verification engine configuration. type Config struct { // Enabled controls whether verification is performed at all. Enabled bool - // Timeout is the maximum duration for a single verification request. + // Timeout is the maximum duration for one finding's complete verification + // operation, including bounded provider-region fallback requests. Timeout time.Duration // Concurrency is the number of concurrent verification workers. @@ -36,10 +43,10 @@ type Config struct { // RateLimit is the maximum verification requests per second. // - // The rate applies both as a per-detector budget (each distinct detector ID - // gets its own limiter of this rate) and as a global ceiling shared across - // all providers, so one high-volume detector type cannot starve the - // verification of unrelated findings in the same run. + // The same rate applies both as a per-detector ceiling (each distinct + // detector ID gets its own limiter) and as a global ceiling shared across all + // providers. The global bucket bounds aggregate traffic; it does not promise + // fair scheduling between providers. RateLimit float64 } @@ -55,7 +62,7 @@ func DefaultConfig() Config { // Engine orchestrates concurrent secret verification with rate limiting. // It maps findings to the appropriate verifier by detector ID and applies -// per-request timeouts and global rate limiting. +// per-finding operation timeouts and global rate limiting. type Engine struct { verifiers map[string]Verifier @@ -90,17 +97,26 @@ func NewEngine(cfg Config, vs []Verifier) *Engine { } vMap := make(map[string]Verifier, len(vs)) - for _, v := range vs { - if _, exists := vMap[v.Type()]; exists { + for i, v := range vs { + if isNilVerifier(v) { + slog.Error("nil verifier ignored while building verification engine", "index", i) + continue + } + id, ok := safeVerifierType(v) + if !ok || id == "" { + slog.Error("invalid verifier ignored while building verification engine", "index", i) + continue + } + if _, exists := vMap[id]; exists { // Unlike registry.Register (which panics), NewEngine tolerates a // duplicate Type() but must not lose it silently: warn so the // capability loss is observable. slog.Warn( "duplicate verifier type; later verifier overwrites earlier", - "verifier_type", v.Type(), + "verifier_type", id, ) } - vMap[v.Type()] = v + vMap[id] = v } burst := int(cfg.RateLimit) @@ -120,6 +136,18 @@ func NewEngine(cfg Config, vs []Verifier) *Engine { } } +func safeVerifierType(v Verifier) (id string, ok bool) { + defer func() { + if recover() != nil { + // Type implementations are not allowed to crash engine construction; + // the recovered value may contain sensitive data and is discarded. + id = "" + ok = false + } + }() + return v.Type(), true +} + // limiterFor returns the per-detector rate limiter for detectorID, creating it // on first use. Each detector gets an independent budget at the configured rate. func (e *Engine) limiterFor(detectorID string) *rate.Limiter { @@ -214,14 +242,8 @@ func (e *Engine) verifySingle(ctx context.Context, pair VerifyPair) finding.Find return f } - // Apply rate limiting: the per-detector limiter first (independent budget - // per provider), then the global ceiling shared across all providers. - if res := e.waitRateLimit(ctx, pair.Raw.DetectorID); res != nil { - f.Verification = *res - return f - } - - // Apply per-request timeout. + // Apply one timeout to the complete provider verification operation, + // including rate-limiter admission and any bounded regional fallback. verifyCtx, cancel := context.WithTimeout(ctx, e.timeout) defer cancel() @@ -231,7 +253,32 @@ func (e *Engine) verifySingle(ctx context.Context, pair VerifyPair) finding.Find "redacted", pair.Raw.Redacted, ) - result := e.safeVerify(verifyCtx, v, pair.Raw) + var result finding.VerificationResult + if gated, ok := v.(RequestGatedVerifier); ok { + requestBudget, err := safeVerificationRequestBudget(gated) + if err != nil { + f.Verification = finding.VerificationResult{ + Status: finding.StatusVerifyError, + Message: "verifier declared an invalid request budget", + } + return f + } + result = e.safeVerifyWithRequestGate( + verifyCtx, + gated, + pair.Raw, + e.boundedRequestGate(verifyCtx, pair.Raw.DetectorID, requestBudget), + ) + } else { + // Standard httpx verifiers receive an admission gate that runs at each + // actual client.Do send point. Format-only, empty-input, and missing- + // context paths therefore consume no limiter token, while a bounded 429 + // replay receives a fresh admission automatically. + requestCtx := httpx.WithRequestGate(verifyCtx, func() *finding.VerificationResult { + return e.waitRateLimit(verifyCtx, pair.Raw.DetectorID) + }) + result = e.safeVerify(requestCtx, v, pair.Raw) + } f.Verification = result slog.Debug( @@ -247,21 +294,116 @@ func (e *Engine) verifySingle(ctx context.Context, pair VerifyPair) finding.Find // admit a request, honoring ctx cancellation. It returns a non-nil result // describing a StatusVerifyError when the context is cancelled while waiting, // and nil when the request may proceed. + func (e *Engine) waitRateLimit(ctx context.Context, detectorID string) *finding.VerificationResult { - for _, l := range []*rate.Limiter{e.limiterFor(detectorID), e.rateLimiter} { - if err := l.Wait(ctx); err != nil { - slog.Warn( - "rate limiter wait cancelled", - "detector_id", detectorID, - "error", err, - ) + if err := ctx.Err(); err != nil { + return rateLimitFailure(detectorID, err) + } + + now := time.Now() + reservations := []*rate.Reservation{ + e.limiterFor(detectorID).ReserveN(now, 1), + e.rateLimiter.ReserveN(now, 1), + } + for _, reservation := range reservations { + if !reservation.OK() { + cancelRateReservations(reservations, now) + return rateLimitFailure(detectorID, fmt.Errorf("rate limiter reservation rejected")) + } + } + + delay := time.Duration(0) + for _, reservation := range reservations { + if candidate := reservation.DelayFrom(now); candidate > delay { + delay = candidate + } + } + if deadline, ok := ctx.Deadline(); ok && now.Add(delay).After(deadline) { + cancelRateReservations(reservations, now) + return rateLimitFailure(detectorID, context.DeadlineExceeded) + } + if delay <= 0 { + if err := ctx.Err(); err != nil { + cancelRateReservations(reservations, now) + return rateLimitFailure(detectorID, err) + } + return nil + } + + timer := time.NewTimer(delay) + defer timer.Stop() + select { + case <-timer.C: + return nil + case <-ctx.Done(): + cancelRateReservations(reservations, time.Now()) + return rateLimitFailure(detectorID, ctx.Err()) + } +} + +func cancelRateReservations(reservations []*rate.Reservation, at time.Time) { + for _, reservation := range reservations { + if reservation != nil && reservation.OK() { + reservation.CancelAt(at) + } + } +} + +func rateLimitFailure(detectorID string, err error) *finding.VerificationResult { + slog.Warn( + "rate limiter wait cancelled", + "detector_id", detectorID, + "error", err, + ) + return &finding.VerificationResult{ + Status: finding.StatusVerifyError, + Message: fmt.Sprintf("rate limiter cancelled: %v", err), + } +} + +func verificationRequestBudget(v RequestGatedVerifier) (int, error) { + budget := v.VerificationRequestBudget() + if budget < 1 { + return 0, fmt.Errorf("verification request budget %d must be positive", budget) + } + if budget > maxVerificationRequestBudget { + return 0, fmt.Errorf("verification request budget %d exceeds maximum %d", budget, maxVerificationRequestBudget) + } + return budget, nil +} + +func safeVerificationRequestBudget(v RequestGatedVerifier) (budget int, err error) { + defer func() { + if recover() != nil { + // The recovered value can contain a raw credential. Do not format, + // return, or log it. + budget = 0 + err = fmt.Errorf("verifier panicked while declaring its request budget") + } + }() + return verificationRequestBudget(v) +} + +// boundedRequestGate returns a concurrency-safe admission gate tied to one +// finding. Each successful invocation consumes one unit of the verifier's +// declared request budget and one token from both the provider and global +// limiters at the actual request send point. +func (e *Engine) boundedRequestGate(ctx context.Context, detectorID string, budget int) RequestGate { + var mu sync.Mutex + used := 0 + return func() *finding.VerificationResult { + mu.Lock() + if used >= budget { + mu.Unlock() return &finding.VerificationResult{ Status: finding.StatusVerifyError, - Message: fmt.Sprintf("rate limiter cancelled: %v", err), + Message: "verifier exceeded its declared request budget", } } + used++ + mu.Unlock() + return e.waitRateLimit(ctx, detectorID) } - return nil } // safeVerify invokes v.Verify with panic recovery so that a single misbehaving @@ -270,13 +412,35 @@ func (e *Engine) waitRateLimit(ctx context.Context, detectorID string) *finding. // findings. A recovered panic is converted to a StatusVerifyError result and // logged with a stack trace; the raw secret is never logged. func (e *Engine) safeVerify(ctx context.Context, v Verifier, raw detector.RawFinding) (result finding.VerificationResult) { + return e.safeVerification(ctx, raw, func() finding.VerificationResult { + return v.Verify(ctx, raw) + }) +} + +func (e *Engine) safeVerifyWithRequestGate( + ctx context.Context, + v RequestGatedVerifier, + raw detector.RawFinding, + gate RequestGate, +) finding.VerificationResult { + return e.safeVerification(ctx, raw, func() finding.VerificationResult { + return v.VerifyWithRequestGate(ctx, raw, gate) + }) +} + +func (e *Engine) safeVerification( + ctx context.Context, + raw detector.RawFinding, + verify func() finding.VerificationResult, +) (result finding.VerificationResult) { defer func() { - if r := recover(); r != nil { - slog.Error( + if recover() != nil { + // Never format or log the recovered payload: a defective verifier can + // panic with the raw credential itself. debug.Stack contains program + // counters/function frames, not panic values or local variables. + slog.ErrorContext(ctx, "verifier panicked; recovered to protect the scan", - "verifier_type", v.Type(), "detector_id", raw.DetectorID, - "panic", fmt.Sprintf("%v", r), "stack", string(debug.Stack()), ) result = finding.VerificationResult{ @@ -285,5 +449,5 @@ func (e *Engine) safeVerify(ctx context.Context, v Verifier, raw detector.RawFin } } }() - return v.Verify(ctx, raw) + return verify() } diff --git a/internal/verifier/engine_test.go b/internal/verifier/engine_test.go index 41b9706..16405dd 100644 --- a/internal/verifier/engine_test.go +++ b/internal/verifier/engine_test.go @@ -1,12 +1,20 @@ package verifier import ( + "bytes" "context" + "log/slog" + "net/http" + "net/http/httptest" + "sync" "sync/atomic" "testing" "time" + "golang.org/x/time/rate" + "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/internal/verifier/internal/httpx" "github.com/HodeTech/leakwatch/pkg/finding" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -26,6 +34,22 @@ type testVerifier struct { maxInFlight atomic.Int64 } +type httpRetryTestVerifier struct { + detectorID string + client *http.Client + url string +} + +func (v *httpRetryTestVerifier) Type() string { return v.detectorID } +func (v *httpRetryTestVerifier) Verify(ctx context.Context, raw detector.RawFinding) finding.VerificationResult { + token := string(raw.Raw) + return httpx.VerifyToken(ctx, v.client, token, httpx.TokenSpec{ + Name: "engine-retry-test", + Request: httpx.Request{URL: v.url, Header: map[string]string{"Authorization": "Bearer " + token}}, + ActiveMessage: "active after bounded retry", + }) +} + func (v *testVerifier) Type() string { return v.detectorID } func (v *testVerifier) Verify(ctx context.Context, _ detector.RawFinding) finding.VerificationResult { @@ -53,17 +77,71 @@ func (v *testVerifier) Verify(ctx context.Context, _ detector.RawFinding) findin return v.result } +// gatedTestVerifier models a verifier that may make several outbound attempts. +// It calls the engine gate immediately before each simulated request and records +// admission timestamps so request pacing can be asserted without real network +// calls. +type gatedTestVerifier struct { + *testVerifier + requestBudget int + budgetPanic string + attempts int + admittedMu sync.Mutex + admittedAt []time.Time +} + +func (v *gatedTestVerifier) VerificationRequestBudget() int { + if v.budgetPanic != "" { + panic(v.budgetPanic) + } + return v.requestBudget +} + +func (v *gatedTestVerifier) VerifyWithRequestGate( + ctx context.Context, + raw detector.RawFinding, + gate RequestGate, +) finding.VerificationResult { + for attempt := 0; attempt < v.attempts; attempt++ { + if rejection := gate(); rejection != nil { + return *rejection + } + v.admittedMu.Lock() + v.admittedAt = append(v.admittedAt, time.Now()) + v.admittedMu.Unlock() + } + return v.Verify(ctx, raw) +} + +func (v *gatedTestVerifier) admissionTimes() []time.Time { + v.admittedMu.Lock() + defer v.admittedMu.Unlock() + return append([]time.Time(nil), v.admittedAt...) +} + // panicVerifier is a mock verifier whose Verify always panics, used to prove the // engine recovers instead of crashing the whole scan. type panicVerifier struct { detectorID string callCount atomic.Int64 + panicRaw bool +} + +type panicTypeVerifier struct{ payload string } + +func (v *panicTypeVerifier) Type() string { panic(v.payload) } + +func (v *panicTypeVerifier) Verify(context.Context, detector.RawFinding) finding.VerificationResult { + return finding.VerificationResult{Status: finding.StatusVerifiedActive} } func (v *panicVerifier) Type() string { return v.detectorID } -func (v *panicVerifier) Verify(context.Context, detector.RawFinding) finding.VerificationResult { +func (v *panicVerifier) Verify(_ context.Context, raw detector.RawFinding) finding.VerificationResult { v.callCount.Add(1) + if v.panicRaw { + panic(string(raw.Raw)) + } panic("boom: simulated verifier defect") } @@ -125,6 +203,204 @@ func TestVerifyAll_MatchingVerifier_UpdatesFinding(t *testing.T) { assert.Equal(t, int64(1), v.callCount.Load()) } +func TestVerifyAll_NoRequestVerifierConsumesNoLimiterCapacity(t *testing.T) { + const detectorID = "format-only-provider" + v := &testVerifier{ + detectorID: detectorID, + result: finding.VerificationResult{ + Status: finding.StatusUnverified, + Message: "format-only verification", + }, + } + engine := NewEngine(Config{Enabled: true, Timeout: time.Second, Concurrency: 1, RateLimit: 1}, []Verifier{v}) + + results := engine.VerifyAll(context.Background(), []VerifyPair{makePair(detectorID, "format****only")}) + require.Len(t, results, 1) + assert.Equal(t, finding.StatusUnverified, results[0].Verification.Status) + assert.Equal(t, int64(1), v.callCount.Load()) + assert.True(t, engine.rateLimiter.Allow(), "a verifier that sends no request must not consume the global token") + _, created := engine.perDetector[detectorID] + assert.False(t, created, "a verifier that sends no request must not create or consume a provider bucket") +} + +func TestVerifyAll_GatedVerifierDoesNotReserveUnusedFallbackToken(t *testing.T) { + base := &testVerifier{ + detectorID: "regional-provider", + result: finding.VerificationResult{Status: finding.StatusVerifiedActive}, + } + v := &gatedTestVerifier{testVerifier: base, requestBudget: 2, attempts: 1} + engine := NewEngine(Config{Enabled: true, Timeout: 250 * time.Millisecond, Concurrency: 1, RateLimit: 1}, []Verifier{v}) + + results := engine.VerifyAll(context.Background(), []VerifyPair{makePair("regional-provider", "regional****key")}) + require.Len(t, results, 1) + assert.Equal(t, finding.StatusVerifiedActive, results[0].Verification.Status) + assert.Equal(t, int64(1), base.callCount.Load()) + assert.Len(t, v.admissionTimes(), 1, "unused fallback must consume no limiter token") +} + +func TestVerifyAll_GatedFallbackRequestsArePacedAtSendTime(t *testing.T) { + base := &testVerifier{ + detectorID: "regional-provider", + result: finding.VerificationResult{Status: finding.StatusVerifiedActive}, + } + v := &gatedTestVerifier{testVerifier: base, requestBudget: 2, attempts: 2} + engine := NewEngine(Config{Enabled: true, Timeout: 2 * time.Second, Concurrency: 1, RateLimit: 1}, []Verifier{v}) + + results := engine.VerifyAll(context.Background(), []VerifyPair{makePair("regional-provider", "regional****key")}) + require.Len(t, results, 1) + assert.Equal(t, finding.StatusVerifiedActive, results[0].Verification.Status) + times := v.admissionTimes() + require.Len(t, times, 2) + assert.GreaterOrEqual(t, times[1].Sub(times[0]), 900*time.Millisecond, + "fallback request must pass the limiter at its actual send point") +} + +func TestVerifyAll_TimeoutIncludesFallbackRateLimitAdmission(t *testing.T) { + base := &testVerifier{ + detectorID: "regional-provider", + result: finding.VerificationResult{Status: finding.StatusVerifiedActive}, + } + v := &gatedTestVerifier{testVerifier: base, requestBudget: 2, attempts: 2} + engine := NewEngine(Config{Enabled: true, Timeout: 50 * time.Millisecond, Concurrency: 1, RateLimit: 1}, []Verifier{v}) + started := time.Now() + + results := engine.VerifyAll(context.Background(), []VerifyPair{makePair("regional-provider", "regional****key")}) + require.Len(t, results, 1) + assert.Equal(t, finding.StatusVerifyError, results[0].Verification.Status) + assert.Less(t, time.Since(started), 500*time.Millisecond, + "configured timeout must bound limiter admission and provider fallback together") + assert.Zero(t, base.callCount.Load(), "provider result must not run after fallback admission fails") +} + +func TestVerificationRequestBudget_IsBounded(t *testing.T) { + tests := []struct { + name string + budget int + want int + wantErr bool + }{ + {name: "zero fails closed", budget: 0, wantErr: true}, + {name: "negative fails closed", budget: -1, wantErr: true}, + {name: "declared budget", budget: 2, want: 2}, + {name: "excessive fails closed", budget: 1000, wantErr: true}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, err := verificationRequestBudget(&gatedTestVerifier{ + testVerifier: &testVerifier{detectorID: "regional-provider"}, + requestBudget: tc.budget, + }) + if tc.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tc.want, got) + }) + } +} + +func TestVerifyAll_ExcessiveRequestBudgetFailsClosed(t *testing.T) { + base := &testVerifier{detectorID: "unsafe-regional-provider", result: finding.VerificationResult{Status: finding.StatusVerifiedActive}} + v := &gatedTestVerifier{testVerifier: base, requestBudget: maxVerificationRequestBudget + 1, attempts: 1} + engine := NewEngine(Config{Enabled: true, Timeout: time.Second, Concurrency: 1, RateLimit: 100}, []Verifier{v}) + + results := engine.VerifyAll(context.Background(), []VerifyPair{makePair(v.detectorID, "unsafe****key")}) + require.Len(t, results, 1) + assert.Equal(t, finding.StatusVerifyError, results[0].Verification.Status) + assert.Zero(t, base.callCount.Load(), "verifier with an unsafe request budget must not run") +} + +func TestVerifyAll_GatedVerifierCannotExceedDeclaredBudget(t *testing.T) { + base := &testVerifier{detectorID: "runaway-provider", result: finding.VerificationResult{Status: finding.StatusVerifiedActive}} + v := &gatedTestVerifier{testVerifier: base, requestBudget: 2, attempts: 3} + engine := NewEngine(Config{Enabled: true, Timeout: time.Second, Concurrency: 1, RateLimit: 1000}, []Verifier{v}) + + results := engine.VerifyAll(context.Background(), []VerifyPair{makePair(v.detectorID, "runaway****key")}) + require.Len(t, results, 1) + assert.Equal(t, finding.StatusVerifyError, results[0].Verification.Status) + assert.Zero(t, base.callCount.Load(), "verifier result must not run after it exceeds the request budget") + assert.Len(t, v.admissionTimes(), 2) +} + +func TestVerifyAll_RequestBudgetPanicFailsClosedWithoutPayload(t *testing.T) { + const canary = "budget-panic-secret-canary-7rQ2mN9p" + base := &testVerifier{detectorID: "panic-budget-provider", result: finding.VerificationResult{Status: finding.StatusVerifiedActive}} + v := &gatedTestVerifier{testVerifier: base, requestBudget: 2, attempts: 1, budgetPanic: canary} + engine := NewEngine(Config{Enabled: true, Timeout: time.Second, Concurrency: 1, RateLimit: 100}, []Verifier{v}) + + results := engine.VerifyAll(context.Background(), []VerifyPair{makePair(v.detectorID, "budget****canary")}) + require.Len(t, results, 1) + assert.Equal(t, finding.StatusVerifyError, results[0].Verification.Status) + assert.NotContains(t, results[0].Verification.Message, canary) + assert.Zero(t, base.callCount.Load()) +} + +func TestWaitRateLimit_GlobalFailureRefundsUnusedProviderReservation(t *testing.T) { + engine := NewEngine(Config{Enabled: true, Timeout: time.Second, Concurrency: 1, RateLimit: 1}, nil) + require.True(t, engine.rateLimiter.Allow(), "consume the global limiter's initial token") + ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) + defer cancel() + + rejection := engine.waitRateLimit(ctx, "refunded-provider") + require.NotNil(t, rejection) + assert.Equal(t, finding.StatusVerifyError, rejection.Status) + + // Replace only the global limiter with an immediately available one. The + // second admission can succeed under a short deadline only if the failed + // combined reservation refunded the provider token. + engine.rateLimiter = rate.NewLimiter(rate.Inf, 1) + secondCtx, secondCancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer secondCancel() + assert.Nil(t, engine.waitRateLimit(secondCtx, "refunded-provider")) +} + +func TestVerifyAll_HTTP429RetryUsesGlobalAndProviderLimiters(t *testing.T) { + for _, tc := range []struct { + name string + globalRate rate.Limit + providerRate rate.Limit + }{ + {name: "provider limiter is enforced", globalRate: 1000, providerRate: 10}, + {name: "global limiter is enforced", globalRate: 10, providerRate: 1000}, + } { + t.Run(tc.name, func(t *testing.T) { + var ( + mu sync.Mutex + callsAt []time.Time + ) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + mu.Lock() + callsAt = append(callsAt, time.Now()) + call := len(callsAt) + mu.Unlock() + if call == 1 { + w.Header().Set("Retry-After", "0") + w.WriteHeader(http.StatusTooManyRequests) + return + } + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + const detectorID = "http-retry-provider" + v := &httpRetryTestVerifier{detectorID: detectorID, client: server.Client(), url: server.URL} + engine := NewEngine(Config{Enabled: true, Timeout: time.Second, Concurrency: 1, RateLimit: 1000}, []Verifier{v}) + engine.rateLimiter = rate.NewLimiter(tc.globalRate, 1) + engine.perDetector[detectorID] = rate.NewLimiter(tc.providerRate, 1) + + results := engine.VerifyAll(context.Background(), []VerifyPair{makePair(detectorID, "http****retry")}) + require.Len(t, results, 1) + assert.Equal(t, finding.StatusVerifiedActive, results[0].Verification.Status) + mu.Lock() + defer mu.Unlock() + require.Len(t, callsAt, 2) + assert.GreaterOrEqual(t, callsAt[1].Sub(callsAt[0]), 85*time.Millisecond, + "retry must pass the limiting bucket at its actual send point") + }) + } +} + func TestVerifyAll_NoMatchingVerifier_LeavesUnverified(t *testing.T) { v := &testVerifier{ detectorID: "aws-access-key-id", @@ -314,6 +590,25 @@ func TestVerifyAll_PanickingVerifier_RecoversAsVerifyError(t *testing.T) { assert.Equal(t, int64(1), pv.callCount.Load()) } +func TestVerifyAll_PanicPayloadCannotLeakRawSecretToLogs(t *testing.T) { + const canary = "panic-payload-secret-canary-9mN2pQ7r" + var logs bytes.Buffer + previous := slog.Default() + slog.SetDefault(slog.New(slog.NewTextHandler(&logs, &slog.HandlerOptions{Level: slog.LevelDebug}))) + t.Cleanup(func() { slog.SetDefault(previous) }) + + pv := &panicVerifier{detectorID: "panic-secret-detector", panicRaw: true} + engine := NewEngine(Config{Enabled: true, Timeout: time.Second, Concurrency: 1, RateLimit: 100}, []Verifier{pv}) + pair := makePair(pv.detectorID, "panic****canary") + pair.Raw.Raw = []byte(canary) + + results := engine.VerifyAll(context.Background(), []VerifyPair{pair}) + require.Len(t, results, 1) + assert.Equal(t, finding.StatusVerifyError, results[0].Verification.Status) + assert.NotContains(t, logs.String(), canary) + assert.Contains(t, logs.String(), "verifier panicked; recovered to protect the scan") +} + func TestVerifyAll_PanickingVerifier_OtherFindingsStillComplete(t *testing.T) { pv := &panicVerifier{detectorID: "panic-detector"} good := &testVerifier{ @@ -389,3 +684,21 @@ func TestNewEngine_DefaultValues_AppliedForZeroConfig(t *testing.T) { assert.Equal(t, DefaultTimeout, engine.timeout) assert.True(t, engine.enabled) } + +func TestNewEngine_InvalidVerifierEntriesCannotCrashConstruction(t *testing.T) { + const canary = "type-panic-secret-canary-5pN8qR2m" + var typedNil *testVerifier + var engine *Engine + assert.NotPanics(t, func() { + engine = NewEngine(Config{Enabled: true}, []Verifier{ + typedNil, + &panicTypeVerifier{payload: canary}, + }) + }) + require.NotNil(t, engine) + assert.Empty(t, engine.verifiers) + + results := engine.VerifyAll(context.Background(), []VerifyPair{makePair("invalid-provider", "invalid****key")}) + require.Len(t, results, 1) + assert.Equal(t, finding.StatusUnverified, results[0].Verification.Status) +} diff --git a/internal/verifier/figma/figma_shared_test.go b/internal/verifier/figma/figma_shared_test.go new file mode 100644 index 0000000..4248d73 --- /dev/null +++ b/internal/verifier/figma/figma_shared_test.go @@ -0,0 +1,16 @@ +package figma + +import ( + "net/http" + "testing" + + "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/internal/verifier" + "github.com/HodeTech/leakwatch/internal/verifier/internal/vtest" +) + +func TestVerify_SharedSafetySuite(t *testing.T) { + vtest.Run(t, vtest.Case{Name: "figma", New: func(url string, client *http.Client) verifier.Verifier { + return &Verifier{apiURL: url, httpClient: client} + }, Raw: detector.RawFinding{DetectorID: detectorID, Raw: []byte("figd_SyntheticFigmaCredentialFixture123456")}}) +} diff --git a/internal/verifier/github/github_oauth_verifier.go b/internal/verifier/github/github_oauth_verifier.go index 49da59c..f1802e3 100644 --- a/internal/verifier/github/github_oauth_verifier.go +++ b/internal/verifier/github/github_oauth_verifier.go @@ -5,8 +5,10 @@ package github import ( "context" "encoding/json" + "fmt" "io" "net/http" + "strings" "github.com/HodeTech/leakwatch/internal/detector" "github.com/HodeTech/leakwatch/internal/verifier" @@ -19,12 +21,29 @@ const oauthDetectorID = "github-oauth-token" // OAuthVerifier checks whether a GitHub OAuth token is active by calling the // GitHub API. It NEVER logs or persists raw token values. type OAuthVerifier struct { - // apiURL overrides the GitHub API base URL (for testing). + // apiURL is reserved for a validated, trusted GitHub.com or GHES API + // origin. The production registration leaves it empty until operator wiring + // exists; tests inject a local server. apiURL string // httpClient overrides the default HTTP client (for testing). httpClient *http.Client } +// NewOAuthForTrustedInstance constructs a subtype-aware GitHub App/OAuth +// verifier for an explicit GitHub.com or GHES API origin. +func NewOAuthForTrustedInstance(instanceURL string) (*OAuthVerifier, error) { + normalized, err := verifier.NormalizeTrustedHTTPSOrigin(instanceURL) + if err != nil { + return nil, err + } + return &OAuthVerifier{apiURL: normalized}, nil +} + +// WithTrustedInstance implements verifier.TrustedInstanceConfigurer. +func (*OAuthVerifier) WithTrustedInstance(instanceURL string) (verifier.Verifier, error) { + return NewOAuthForTrustedInstance(instanceURL) +} + func init() { verifier.Register(&OAuthVerifier{}) } @@ -38,30 +57,75 @@ func (v *OAuthVerifier) Type() string { // Raw contains the token value. func (v *OAuthVerifier) Verify(ctx context.Context, raw detector.RawFinding) finding.VerificationResult { token := string(raw.Raw) - apiURL := httpx.BaseURL(v.apiURL, defaultAPIURL) + if token == "" { + return finding.VerificationResult{Status: finding.StatusUnverified, Message: "empty token"} + } + if v.apiURL == "" { + return finding.VerificationResult{ + Status: finding.StatusUnverified, + Message: "trusted GitHub API origin is not configured", + } + } + apiURL := v.apiURL + path := "/user" + var decode httpx.DecodeFunc + var decodeResponse httpx.ResponseDecodeFunc + switch { + case strings.HasPrefix(token, "gho_"), strings.HasPrefix(token, "ghu_"): + // OAuth and GitHub App user access tokens authenticate a user. + decodeResponse = decodeUserResponse + case strings.HasPrefix(token, "ghs_"): + // Installation tokens do not support GET /user. This endpoint is a + // read-only identity-safe probe and works even when the installation has + // access to zero repositories. + path = "/installation/repositories" + decode = decodeInstallationRepositories + case strings.HasPrefix(token, "ghr_"): + // Verifying a refresh token requires exchanging it, which rotates the + // token. A verifier must never create that side effect. + return finding.VerificationResult{ + Status: finding.StatusUnverified, + Message: "GitHub refresh token cannot be verified without rotating it", + } + default: + return finding.VerificationResult{ + Status: finding.StatusUnverified, + Message: "unsupported GitHub token subtype", + } + } return httpx.VerifyToken(ctx, v.httpClient, token, httpx.TokenSpec{ Name: "github oauth", Request: httpx.Request{ - URL: apiURL + "/user", + URL: apiURL + path, Header: map[string]string{ "Authorization": "Bearer " + token, "Accept": "application/vnd.github+json", }, }, - ActiveMessage: "GitHub OAuth token is active", - InactiveMessage: "GitHub OAuth token is invalid or revoked", - Decode: decodeOAuthUser, + ActiveMessage: "GitHub OAuth or installation token is active", + InactiveMessage: "GitHub OAuth or installation token is invalid or revoked", + Decode: decode, + DecodeResponse: decodeResponse, + DecodeInactive: decodeGitHubInactive, + RequireCompleteBody: true, + RequireJSONContentType: true, }) } -// decodeOAuthUser parses the GitHub API response for a valid OAuth token. -func decodeOAuthUser(body io.Reader) (map[string]string, string, error) { - var user struct { - Login string `json:"login"` +func decodeInstallationRepositories(body io.Reader) (map[string]string, string, error) { + var response struct { + TotalCount *int `json:"total_count"` } - if err := json.NewDecoder(body).Decode(&user); err != nil { + decoder := json.NewDecoder(body) + if err := decoder.Decode(&response); err != nil { return nil, "", err } - return map[string]string{"login": user.Login}, "", nil + if err := requireGitHubJSONEOF(decoder); err != nil { + return nil, "", err + } + if response.TotalCount == nil || *response.TotalCount < 0 { + return nil, "", fmt.Errorf("missing GitHub installation repository count") + } + return map[string]string{"token_subtype": "ghs"}, "", nil } diff --git a/internal/verifier/github/github_oauth_verifier_test.go b/internal/verifier/github/github_oauth_verifier_test.go index 35b8e9a..ea75fed 100644 --- a/internal/verifier/github/github_oauth_verifier_test.go +++ b/internal/verifier/github/github_oauth_verifier_test.go @@ -4,6 +4,7 @@ import ( "context" "net/http" "net/http/httptest" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -20,6 +21,8 @@ func TestOAuthVerify_ValidToken_ReturnsActive(t *testing.T) { assert.Equal(t, "application/vnd.github+json", r.Header.Get("Accept")) w.Header().Set("Content-Type", "application/json") + w.Header().Set("X-OAuth-Scopes", "user, repo, user") + w.Header().Set("GitHub-Authentication-Token-Expiration", "2027-01-02 03:04:05 UTC") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"login":"octocat"}`)) })) @@ -39,12 +42,61 @@ func TestOAuthVerify_ValidToken_ReturnsActive(t *testing.T) { result := v.Verify(context.Background(), raw) require.Equal(t, finding.StatusVerifiedActive, result.Status) - assert.Equal(t, "GitHub OAuth token is active", result.Message) + assert.Equal(t, "GitHub OAuth or installation token is active", result.Message) assert.Equal(t, "octocat", result.ExtraData["login"]) + assert.Equal(t, "repo,user", result.ExtraData["scopes"]) + assert.Equal(t, "2", result.ExtraData["scope_count"]) + assert.Equal(t, "2027-01-02T03:04:05Z", result.ExtraData["expires_at"]) +} + +func TestDecodeInstallationRepositories_RejectsMalformedSuccess(t *testing.T) { + for _, body := range []string{ + `{"total_count":-1}`, + `{"total_count":0} {"extra":true}`, + } { + _, _, err := decodeInstallationRepositories(strings.NewReader(body)) + assert.Error(t, err, body) + } +} + +func TestOAuthVerify_InstallationToken_UsesInstallationEndpoint(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/installation/repositories", r.URL.Path) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"total_count":0,"repositories":[]}`)) + })) + defer server.Close() + + result := (&OAuthVerifier{apiURL: server.URL, httpClient: server.Client()}).Verify( + context.Background(), + detector.RawFinding{Raw: []byte("ghs_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef12")}, + ) + + require.Equal(t, finding.StatusVerifiedActive, result.Status) + assert.Equal(t, "ghs", result.ExtraData["token_subtype"]) +} + +func TestOAuthVerify_RefreshToken_IsUnverifiedWithoutRequest(t *testing.T) { + requests := 0 + server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { + requests++ + })) + defer server.Close() + + result := (&OAuthVerifier{apiURL: server.URL, httpClient: server.Client()}).Verify( + context.Background(), + detector.RawFinding{Raw: []byte("ghr_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef12")}, + ) + + assert.Equal(t, finding.StatusUnverified, result.Status) + assert.Contains(t, result.Message, "cannot be verified without rotating") + assert.Zero(t, requests) } func TestOAuthVerify_InvalidToken_ReturnsInactive(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"message":"Bad credentials"}`)) })) @@ -64,7 +116,20 @@ func TestOAuthVerify_InvalidToken_ReturnsInactive(t *testing.T) { result := v.Verify(context.Background(), raw) assert.Equal(t, finding.StatusVerifiedInactive, result.Status) - assert.Equal(t, "GitHub OAuth token is invalid or revoked", result.Message) + assert.Equal(t, "GitHub OAuth or installation token is invalid or revoked", result.Message) +} + +func TestOAuthVerify_InactiveHTMLIsInconclusive(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`proxy login`)) + })) + defer server.Close() + result := (&OAuthVerifier{apiURL: server.URL, httpClient: server.Client()}).Verify( + context.Background(), detector.RawFinding{Raw: []byte("gho_synthetic123456789012345678901234")}, + ) + assert.Equal(t, finding.StatusVerifyError, result.Status) } func TestOAuthVerify_ServerError_ReturnsError(t *testing.T) { @@ -143,6 +208,14 @@ func TestOAuthVerify_EmptyToken_ReturnsUnverified(t *testing.T) { assert.Equal(t, "empty token", result.Message) } +func TestOAuthVerify_WithoutTrustedOrigin_MakesNoRequest(t *testing.T) { + v := &OAuthVerifier{} + result := v.Verify(context.Background(), detector.RawFinding{Raw: []byte("synthetic-github-oauth-token")}) + + assert.Equal(t, finding.StatusUnverified, result.Status) + assert.Equal(t, "trusted GitHub API origin is not configured", result.Message) +} + func TestOAuthVerify_MalformedJSON_ReturnsVerifyError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -169,3 +242,39 @@ func TestOAuthVerify_MalformedJSON_ReturnsVerifyError(t *testing.T) { assert.Equal(t, finding.StatusVerifyError, result.Status) assert.Contains(t, result.Message, "failed to decode response body") } + +func TestOAuthVerify_MissingIdentityOrWrongContentType_ReturnsVerifyError(t *testing.T) { + tests := []struct { + name string + contentType string + body string + }{ + {name: "missing login", contentType: "application/json", body: `{}`}, + {name: "wrong content type", contentType: "text/plain", body: `{"login":"octocat"}`}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", tc.contentType) + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(tc.body)) + })) + defer server.Close() + + result := (&OAuthVerifier{apiURL: server.URL, httpClient: server.Client()}).Verify( + context.Background(), detector.RawFinding{Raw: []byte("gho_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef12")}, + ) + assert.Equal(t, finding.StatusVerifyError, result.Status) + }) + } +} + +func TestNewOAuthForTrustedInstance_ValidatesOrigin(t *testing.T) { + configured, err := NewOAuthForTrustedInstance("https://api.github.example/") + require.NoError(t, err) + assert.Equal(t, "https://api.github.example", configured.apiURL) + for _, origin := range []string{"http://api.github.com", "https://127.0.0.1", "https://localhost", "https://github.example/path"} { + _, err := NewOAuthForTrustedInstance(origin) + assert.Error(t, err, origin) + } +} diff --git a/internal/verifier/github/github_verifier.go b/internal/verifier/github/github_verifier.go index d4c2707..922db7e 100644 --- a/internal/verifier/github/github_verifier.go +++ b/internal/verifier/github/github_verifier.go @@ -5,8 +5,13 @@ package github import ( "context" "encoding/json" + "fmt" "io" "net/http" + "sort" + "strconv" + "strings" + "time" "github.com/HodeTech/leakwatch/internal/detector" "github.com/HodeTech/leakwatch/internal/verifier" @@ -16,18 +21,32 @@ import ( const detectorID = "github-token" -// defaultAPIURL is the base URL for the GitHub API. -const defaultAPIURL = "https://api.github.com" - // Verifier checks whether a GitHub token is active by calling the // GitHub API. It NEVER logs or persists raw token values. type Verifier struct { - // apiURL overrides the GitHub API base URL (for testing). + // apiURL is reserved for a validated, trusted GitHub.com or GHES API + // origin. The production registration leaves it empty until operator wiring + // exists; tests inject a local server. apiURL string // httpClient overrides the default HTTP client (for testing). httpClient *http.Client } +// NewForTrustedInstance constructs a GitHub PAT verifier for an origin chosen +// explicitly by the operator (GitHub.com API or a GHES API origin). +func NewForTrustedInstance(instanceURL string) (*Verifier, error) { + normalized, err := verifier.NormalizeTrustedHTTPSOrigin(instanceURL) + if err != nil { + return nil, err + } + return &Verifier{apiURL: normalized}, nil +} + +// WithTrustedInstance implements verifier.TrustedInstanceConfigurer. +func (*Verifier) WithTrustedInstance(instanceURL string) (verifier.Verifier, error) { + return NewForTrustedInstance(instanceURL) +} + func init() { verifier.Register(&Verifier{}) } @@ -41,7 +60,16 @@ func (v *Verifier) Type() string { // Raw contains the token value. func (v *Verifier) Verify(ctx context.Context, raw detector.RawFinding) finding.VerificationResult { token := string(raw.Raw) - apiURL := httpx.BaseURL(v.apiURL, defaultAPIURL) + if token == "" { + return finding.VerificationResult{Status: finding.StatusUnverified, Message: "empty token"} + } + if v.apiURL == "" { + return finding.VerificationResult{ + Status: finding.StatusUnverified, + Message: "trusted GitHub API origin is not configured", + } + } + apiURL := v.apiURL return httpx.VerifyToken(ctx, v.httpClient, token, httpx.TokenSpec{ Name: "github", @@ -52,26 +80,118 @@ func (v *Verifier) Verify(ctx context.Context, raw detector.RawFinding) finding. "Accept": "application/vnd.github+json", }, }, - ActiveMessage: "GitHub token is active", - InactiveMessage: "GitHub token is invalid or revoked", - Decode: decodeUser, + ActiveMessage: "GitHub token is active", + InactiveMessage: "GitHub token is invalid or revoked", + DecodeResponse: decodeUserResponse, + DecodeInactive: decodeGitHubInactive, + RequireCompleteBody: true, + RequireJSONContentType: true, }) } +func decodeGitHubInactive(body io.Reader) error { + var response struct { + Message string `json:"message"` + } + decoder := json.NewDecoder(body) + if err := decoder.Decode(&response); err != nil { + return err + } + if strings.TrimSpace(response.Message) != "Bad credentials" { + return fmt.Errorf("unexpected GitHub authentication error") + } + return requireGitHubJSONEOF(decoder) +} + +// decodeUserResponse parses the required identity body and enriches classic PAT +// findings with the provider's documented OAuth-scope and token-expiry response +// headers. Fine-grained tokens may omit X-OAuth-Scopes; absence is not an error. +func decodeUserResponse(header http.Header, body io.Reader) (map[string]string, string, error) { + extra, downgrade, err := decodeUser(body) + if err != nil { + return nil, "", err + } + if scopes := normalizedScopes(header.Get("X-OAuth-Scopes")); len(scopes) > 0 { + extra["scopes"] = strings.Join(scopes, ",") + extra["scope_count"] = strconv.Itoa(len(scopes)) + } + if expiresAt, ok := normalizedGitHubExpiry(header.Get("GitHub-Authentication-Token-Expiration")); ok { + extra["expires_at"] = expiresAt + } + return extra, downgrade, nil +} + // decodeUser parses the GitHub API response for a valid token. -// -// Enhancement note: a classic PAT's granted scopes are exposed by GitHub in -// the X-OAuth-Scopes response header, not the body, and would be valuable -// triage signal (a repo-scoped token is materially more critical than a -// read:user-scoped one). Surfacing it requires httpx.DecodeFunc to receive -// the response headers, which it currently does not (body-only) — that is a -// change to internal/verifier/internal/httpx, outside this package. func decodeUser(body io.Reader) (map[string]string, string, error) { var user struct { Login string `json:"login"` } - if err := json.NewDecoder(body).Decode(&user); err != nil { + decoder := json.NewDecoder(body) + if err := decoder.Decode(&user); err != nil { + return nil, "", err + } + user.Login = strings.TrimSpace(user.Login) + if user.Login == "" { + return nil, "", fmt.Errorf("missing GitHub user login") + } + if err := requireGitHubJSONEOF(decoder); err != nil { return nil, "", err } return map[string]string{"login": user.Login}, "", nil } + +func requireGitHubJSONEOF(decoder *json.Decoder) error { + var trailing any + if err := decoder.Decode(&trailing); err != io.EOF { + if err == nil { + return fmt.Errorf("GitHub response contains trailing JSON values") + } + return fmt.Errorf("invalid trailing GitHub response: %w", err) + } + return nil +} + +func normalizedScopes(raw string) []string { + if len(raw) > 4096 { + return nil + } + seen := make(map[string]struct{}) + for _, candidate := range strings.Split(raw, ",") { + scope := strings.TrimSpace(candidate) + if scope == "" || len(scope) > 64 || !isSafeScope(scope) { + continue + } + seen[scope] = struct{}{} + if len(seen) >= 128 { + break + } + } + scopes := make([]string, 0, len(seen)) + for scope := range seen { + scopes = append(scopes, scope) + } + sort.Strings(scopes) + return scopes +} + +func isSafeScope(scope string) bool { + for _, char := range scope { + if (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || + (char >= '0' && char <= '9') || char == ':' || char == '_' || char == '-' { + continue + } + return false + } + return true +} + +func normalizedGitHubExpiry(raw string) (string, bool) { + value := strings.TrimSpace(raw) + for _, layout := range []string{time.RFC3339, "2006-01-02 15:04:05 MST"} { + parsed, err := time.Parse(layout, value) + if err == nil { + return parsed.UTC().Format(time.RFC3339), true + } + } + return "", false +} diff --git a/internal/verifier/github/github_verifier_test.go b/internal/verifier/github/github_verifier_test.go index 32b2c98..f0caded 100644 --- a/internal/verifier/github/github_verifier_test.go +++ b/internal/verifier/github/github_verifier_test.go @@ -4,6 +4,7 @@ import ( "context" "net/http" "net/http/httptest" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -19,6 +20,8 @@ func TestVerify_ValidToken_ReturnsActive(t *testing.T) { assert.Contains(t, r.Header.Get("Authorization"), "Bearer ") w.Header().Set("Content-Type", "application/json") + w.Header().Set("X-OAuth-Scopes", "user, repo, user") + w.Header().Set("GitHub-Authentication-Token-Expiration", "2027-01-02 03:04:05 UTC") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"login":"octocat","id":1}`)) })) @@ -40,10 +43,25 @@ func TestVerify_ValidToken_ReturnsActive(t *testing.T) { require.Equal(t, finding.StatusVerifiedActive, result.Status) assert.Equal(t, "GitHub token is active", result.Message) assert.Equal(t, "octocat", result.ExtraData["login"]) + assert.Equal(t, "repo,user", result.ExtraData["scopes"]) + assert.Equal(t, "2", result.ExtraData["scope_count"]) + assert.Equal(t, "2027-01-02T03:04:05Z", result.ExtraData["expires_at"]) +} + +func TestDecodeUserResponse_IgnoresMalformedUntrustedMetadata(t *testing.T) { + header := http.Header{ + "X-Oauth-Scopes": []string{"repo, scope with spaces, bad\nvalue"}, + "Github-Authentication-Token-Expiration": []string{"not-a-date"}, + } + extra, _, err := decodeUserResponse(header, strings.NewReader(`{"login":"octocat"}`)) + require.NoError(t, err) + assert.Equal(t, "repo", extra["scopes"]) + assert.NotContains(t, extra, "expires_at") } func TestVerify_InvalidToken_ReturnsInactive(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"message":"Bad credentials"}`)) })) @@ -66,6 +84,31 @@ func TestVerify_InvalidToken_ReturnsInactive(t *testing.T) { assert.Equal(t, "GitHub token is invalid or revoked", result.Message) } +func TestVerify_InactiveResponseMustBeDefinitiveJSON(t *testing.T) { + tests := []struct { + name string + contentType string + body string + }{ + {name: "proxy html", contentType: "text/html", body: `login`}, + {name: "ambiguous json", contentType: "application/json", body: `{"message":"Unauthorized"}`}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", tc.contentType) + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(tc.body)) + })) + defer server.Close() + result := (&Verifier{apiURL: server.URL, httpClient: server.Client()}).Verify( + context.Background(), detector.RawFinding{Raw: []byte("ghp_synthetic123456789012345678901234")}, + ) + assert.Equal(t, finding.StatusVerifyError, result.Status) + }) + } +} + func TestVerify_ServerError_ReturnsError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) @@ -109,3 +152,47 @@ func TestVerify_EmptyToken_ReturnsUnverified(t *testing.T) { assert.Equal(t, finding.StatusUnverified, result.Status) assert.Equal(t, "empty token", result.Message) } + +func TestVerify_WithoutTrustedOrigin_MakesNoRequest(t *testing.T) { + v := &Verifier{} + result := v.Verify(context.Background(), detector.RawFinding{Raw: []byte("synthetic-github-token")}) + + assert.Equal(t, finding.StatusUnverified, result.Status) + assert.Equal(t, "trusted GitHub API origin is not configured", result.Message) +} + +func TestVerify_MissingIdentityOrWrongContentType_ReturnsVerifyError(t *testing.T) { + tests := []struct { + name string + contentType string + body string + }{ + {name: "missing login", contentType: "application/json", body: `{}`}, + {name: "wrong content type", contentType: "text/html", body: `{"login":"octocat"}`}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", tc.contentType) + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(tc.body)) + })) + defer server.Close() + + result := (&Verifier{apiURL: server.URL, httpClient: server.Client()}).Verify( + context.Background(), detector.RawFinding{Raw: []byte("ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef12")}, + ) + assert.Equal(t, finding.StatusVerifyError, result.Status) + }) + } +} + +func TestNewForTrustedInstance_ValidatesOrigin(t *testing.T) { + configured, err := NewForTrustedInstance("https://api.github.example/") + require.NoError(t, err) + assert.Equal(t, "https://api.github.example", configured.apiURL) + for _, origin := range []string{"http://api.github.com", "https://127.0.0.1", "https://localhost", "https://github.example/path"} { + _, err := NewForTrustedInstance(origin) + assert.Error(t, err, origin) + } +} diff --git a/internal/verifier/gitlab/gitlab_verifier.go b/internal/verifier/gitlab/gitlab_verifier.go index d72012a..4e1bf7e 100644 --- a/internal/verifier/gitlab/gitlab_verifier.go +++ b/internal/verifier/gitlab/gitlab_verifier.go @@ -5,8 +5,13 @@ package gitlab import ( "context" "encoding/json" + "fmt" "io" "net/http" + "sort" + "strconv" + "strings" + "time" "github.com/HodeTech/leakwatch/internal/detector" "github.com/HodeTech/leakwatch/internal/verifier" @@ -16,9 +21,12 @@ import ( const detectorID = "gitlab-pat" -// defaultHost is the GitLab SaaS host, used when no self-hosted instance host -// was captured alongside the token. -const defaultHost = "gitlab.com" +type gitLabGranularScope struct { + Access string `json:"access"` + Permissions []string `json:"permissions"` + ProjectID *int64 `json:"project_id"` + GroupID *int64 `json:"group_id"` +} // Verifier checks whether a GitLab personal access token is active by calling // the GitLab API. It NEVER logs or persists raw token values. @@ -29,6 +37,24 @@ type Verifier struct { httpClient *http.Client } +var _ verifier.RequestGatedVerifier = (*Verifier)(nil) + +// NewForTrustedInstance constructs a GitLab verifier for an API origin +// explicitly supplied by the operator. Finding metadata and scanned URLs are +// deliberately never accepted as routing authority. +func NewForTrustedInstance(instanceURL string) (*Verifier, error) { + normalized, err := verifier.NormalizeTrustedHTTPSOrigin(instanceURL) + if err != nil { + return nil, err + } + return &Verifier{apiURL: normalized}, nil +} + +// WithTrustedInstance implements verifier.TrustedInstanceConfigurer. +func (*Verifier) WithTrustedInstance(instanceURL string) (verifier.Verifier, error) { + return NewForTrustedInstance(instanceURL) +} + func init() { verifier.Register(&Verifier{}) } @@ -38,48 +64,296 @@ func (v *Verifier) Type() string { return detectorID } +// VerificationRequestBudget covers the identity probe, the optional PAT +// self-metadata probe, and at most one bounded 429 retry for each GET. +func (*Verifier) VerificationRequestBudget() int { return 4 } + // Verify checks if the detected GitLab personal access token is valid/active. -// Raw contains the token value. The API host is derived from a co-located -// self-hosted instance host (ExtraData["host"]) when present, defaulting to -// gitlab.com — so an active self-hosted token is verified against its true -// issuer instead of being misreported as invalid by gitlab.com. +// Raw contains the token value. Only personal access tokens (`glpat-`) can use +// the read-only `/user` probe. Other GitLab credential families have different +// authentication contracts and remain unverified. func (v *Verifier) Verify(ctx context.Context, raw detector.RawFinding) finding.VerificationResult { + return v.verify(ctx, raw, nil) +} + +// VerifyWithRequestGate admits every actual identity/metadata request (and any +// bounded GET retry) at its send point. +func (v *Verifier) VerifyWithRequestGate( + ctx context.Context, + raw detector.RawFinding, + gate verifier.RequestGate, +) finding.VerificationResult { + return v.verify(ctx, raw, gate) +} + +func (v *Verifier) verify( + ctx context.Context, + raw detector.RawFinding, + gate verifier.RequestGate, +) finding.VerificationResult { token := string(raw.Raw) - apiURL := v.baseURL(raw) + if token == "" { + return finding.VerificationResult{Status: finding.StatusUnverified, Message: "empty token"} + } + if !strings.HasPrefix(token, "glpat-") { + return finding.VerificationResult{ + Status: finding.StatusUnverified, + Message: "GitLab token subtype has no safe universal verification probe", + } + } + if v.apiURL == "" { + return finding.VerificationResult{ + Status: finding.StatusUnverified, + Message: "trusted GitLab API origin is not configured", + } + } + if rejection := admitGitLabRequest(gate); rejection != nil { + return *rejection + } + requestCtx := ctx + if gate != nil { + requestCtx = httpx.WithRetryGate(ctx, httpx.RetryGate(gate)) + } - return httpx.VerifyToken(ctx, v.httpClient, token, httpx.TokenSpec{ + result := httpx.VerifyToken(requestCtx, v.httpClient, token, httpx.TokenSpec{ Name: "gitlab", Request: httpx.Request{ - URL: apiURL + "/api/v4/user", + URL: v.apiURL + "/api/v4/user", Header: map[string]string{"PRIVATE-TOKEN": token}, }, - ActiveMessage: "GitLab token is active", - InactiveMessage: "GitLab token is invalid or revoked", - Decode: decodeUser, + ActiveMessage: "GitLab token is active", + InactiveMessage: "GitLab token is invalid or revoked", + Decode: decodeUser, + DecodeInactive: decodeUnauthorized, + RequireCompleteBody: true, + RequireJSONContentType: true, }) -} + if result.Status != finding.StatusVerifiedActive { + return result + } -// baseURL resolves the GitLab API base URL. A test-injected apiURL wins; -// otherwise the co-located self-hosted host (ExtraData["host"]) is used, falling -// back to gitlab.com. raw.ExtraData may be nil (safe to index). -func (v *Verifier) baseURL(raw detector.RawFinding) string { - if v.apiURL != "" { - return v.apiURL + // Token metadata is optional enrichment. Older/self-managed instances and + // non-personal glpat token families may not expose the PAT self endpoint; + // the proven-active /user result remains authoritative in those cases. + if rejection := admitGitLabRequest(gate); rejection != nil { + return result } - host := defaultHost - if h := raw.ExtraData["host"]; h != "" { - host = h + metadata := httpx.VerifyToken(requestCtx, v.httpClient, token, httpx.TokenSpec{ + Name: "gitlab-token-metadata", + Request: httpx.Request{ + URL: v.apiURL + "/api/v4/personal_access_tokens/self", + Header: map[string]string{"PRIVATE-TOKEN": token}, + }, + ActiveMessage: "GitLab token metadata is available", + InactiveMessage: "GitLab token metadata is unavailable", + Decode: decodeTokenMetadata, + RequireCompleteBody: true, + RequireJSONContentType: true, + }) + if metadata.Status == finding.StatusVerifiedActive { + if result.ExtraData == nil { + result.ExtraData = make(map[string]string) + } + for key, value := range metadata.ExtraData { + result.ExtraData[key] = value + } + } + return result +} + +func admitGitLabRequest(gate verifier.RequestGate) *finding.VerificationResult { + if gate == nil { + return nil } - return "https://" + host + return gate() } // decodeUser parses the GitLab API response for a valid token. func decodeUser(body io.Reader) (map[string]string, string, error) { var user struct { + ID int64 `json:"id"` Username string `json:"username"` } - if err := json.NewDecoder(body).Decode(&user); err != nil { + decoder := json.NewDecoder(body) + if err := decoder.Decode(&user); err != nil { return nil, "", err } + if err := requireJSONEOF(decoder); err != nil { + return nil, "", err + } + if user.ID <= 0 || strings.TrimSpace(user.Username) == "" { + return nil, "", fmt.Errorf("GitLab user response is missing required identity fields") + } return map[string]string{"username": user.Username}, "", nil } + +func decodeUnauthorized(body io.Reader) error { + var response struct { + Message string `json:"message"` + } + decoder := json.NewDecoder(body) + if err := decoder.Decode(&response); err != nil { + return err + } + if err := requireJSONEOF(decoder); err != nil { + return err + } + if response.Message != "401 Unauthorized" { + return fmt.Errorf("GitLab response is not a standard invalid-token response") + } + return nil +} + +func decodeTokenMetadata(body io.Reader) (map[string]string, string, error) { + var metadata struct { + Active bool `json:"active"` + Revoked bool `json:"revoked"` + Scopes []string `json:"scopes"` + GranularScopes []gitLabGranularScope `json:"granular_scopes"` + ExpiresAt *string `json:"expires_at"` + } + decoder := json.NewDecoder(body) + if err := decoder.Decode(&metadata); err != nil { + return nil, "", err + } + if err := requireJSONEOF(decoder); err != nil { + return nil, "", err + } + if !metadata.Active || metadata.Revoked { + return nil, "", fmt.Errorf("GitLab self metadata did not describe an active token") + } + extra := make(map[string]string) + scopes, err := normalizedTokenScopes(metadata.Scopes, metadata.GranularScopes) + if err != nil { + return nil, "", err + } + if len(scopes) > 0 { + extra["scopes"] = strings.Join(scopes, ",") + extra["scope_count"] = strconv.Itoa(len(scopes)) + } + if metadata.ExpiresAt != nil && strings.TrimSpace(*metadata.ExpiresAt) != "" { + expiresAt := strings.TrimSpace(*metadata.ExpiresAt) + if _, err := time.Parse(time.DateOnly, expiresAt); err != nil { + return nil, "", fmt.Errorf("GitLab token expiry is not an ISO date") + } + extra["expires_at"] = expiresAt + } + return extra, "", nil +} + +func normalizedTokenScopes(standard []string, granular []gitLabGranularScope) ([]string, error) { + result := normalizedMetadataScopes(standard) + if len(granular) > 128 { + return nil, fmt.Errorf("GitLab granular scope list exceeds safe limit") + } + seen := make(map[string]struct{}, len(result)+len(granular)) + for _, scope := range result { + seen[scope] = struct{}{} + } + for _, scope := range granular { + labels, err := normalizedGranularScopeLabels(scope) + if err != nil { + return nil, err + } + for _, label := range labels { + seen[label] = struct{}{} + if len(seen) > 128 { + return nil, fmt.Errorf("GitLab normalized scope list exceeds safe limit") + } + } + } + result = result[:0] + for scope := range seen { + result = append(result, scope) + } + sort.Strings(result) + return result, nil +} + +func normalizedGranularScopeLabels(scope gitLabGranularScope) ([]string, error) { + access := strings.TrimSpace(scope.Access) + if !isAllowedGranularAccess(access) || len(scope.Permissions) == 0 || len(scope.Permissions) > 128 { + return nil, fmt.Errorf("GitLab granular scope has an invalid access contract") + } + target, err := granularScopeTarget(access, scope.ProjectID, scope.GroupID) + if err != nil { + return nil, err + } + labels := make([]string, 0, len(scope.Permissions)) + for _, candidate := range scope.Permissions { + permission := strings.TrimSpace(candidate) + if permission == "" || len(permission) > 64 || !isSafeMetadataScope(permission) { + return nil, fmt.Errorf("GitLab granular scope has an invalid permission") + } + labels = append(labels, "granular:"+access+target+":"+permission) + } + return labels, nil +} + +func granularScopeTarget(access string, projectID, groupID *int64) (string, error) { + if access != "selected_memberships" { + if projectID != nil || groupID != nil { + return "", fmt.Errorf("GitLab granular scope has an unexpected membership target") + } + return "", nil + } + switch { + case projectID != nil && *projectID > 0 && groupID == nil: + return ":project:" + strconv.FormatInt(*projectID, 10), nil + case groupID != nil && *groupID > 0 && projectID == nil: + return ":group:" + strconv.FormatInt(*groupID, 10), nil + default: + return "", fmt.Errorf("GitLab selected-membership scope has an invalid target") + } +} + +func isAllowedGranularAccess(access string) bool { + switch access { + case "selected_memberships", "user", "instance", "all_memberships", "personal_projects": + return true + default: + return false + } +} + +func normalizedMetadataScopes(input []string) []string { + seen := make(map[string]struct{}) + for _, candidate := range input { + scope := strings.TrimSpace(candidate) + if scope == "" || len(scope) > 64 || !isSafeMetadataScope(scope) { + continue + } + seen[scope] = struct{}{} + if len(seen) >= 128 { + break + } + } + result := make([]string, 0, len(seen)) + for scope := range seen { + result = append(result, scope) + } + sort.Strings(result) + return result +} + +func isSafeMetadataScope(scope string) bool { + for _, char := range scope { + if (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || + (char >= '0' && char <= '9') || char == ':' || char == '_' || char == '-' { + continue + } + return false + } + return true +} + +func requireJSONEOF(decoder *json.Decoder) error { + var trailing any + if err := decoder.Decode(&trailing); err != io.EOF { + if err == nil { + return fmt.Errorf("response contains trailing JSON values") + } + return err + } + return nil +} diff --git a/internal/verifier/gitlab/gitlab_verifier_test.go b/internal/verifier/gitlab/gitlab_verifier_test.go index 09d25e6..8488fb9 100644 --- a/internal/verifier/gitlab/gitlab_verifier_test.go +++ b/internal/verifier/gitlab/gitlab_verifier_test.go @@ -4,6 +4,7 @@ import ( "context" "net/http" "net/http/httptest" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -15,12 +16,17 @@ import ( func TestVerify_ValidToken_ReturnsActive(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/api/v4/user", r.URL.Path) assert.NotEmpty(t, r.Header.Get("PRIVATE-TOKEN")) w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(`{"id":1,"username":"johndoe","name":"John Doe"}`)) + switch r.URL.Path { + case "/api/v4/user": + _, _ = w.Write([]byte(`{"id":1,"username":"johndoe","name":"John Doe"}`)) + case "/api/v4/personal_access_tokens/self": + _, _ = w.Write([]byte(`{"active":true,"revoked":false,"scopes":["read_api","api","api"],"expires_at":"2027-02-03"}`)) + default: + http.NotFound(w, r) + } })) defer server.Close() @@ -40,10 +46,120 @@ func TestVerify_ValidToken_ReturnsActive(t *testing.T) { require.Equal(t, finding.StatusVerifiedActive, result.Status) assert.Equal(t, "GitLab token is active", result.Message) assert.Equal(t, "johndoe", result.ExtraData["username"]) + assert.Equal(t, "api,read_api", result.ExtraData["scopes"]) + assert.Equal(t, "2", result.ExtraData["scope_count"]) + assert.Equal(t, "2027-02-03", result.ExtraData["expires_at"]) +} + +func TestVerifyWithRequestGate_MetadataFailureDoesNotEraseProvenActiveIdentity(t *testing.T) { + requests := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requests++ + w.Header().Set("Content-Type", "application/json") + if r.URL.Path == "/api/v4/user" { + _, _ = w.Write([]byte(`{"id":1,"username":"johndoe"}`)) + return + } + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"message":"404 Not Found"}`)) + })) + defer server.Close() + + admissions := 0 + result := (&Verifier{apiURL: server.URL, httpClient: server.Client()}).VerifyWithRequestGate( + context.Background(), + detector.RawFinding{Raw: []byte("glpat-ABCDEFGHIJKLMNOPQRSTUVWXYZab")}, + func() *finding.VerificationResult { admissions++; return nil }, + ) + require.Equal(t, finding.StatusVerifiedActive, result.Status) + assert.Equal(t, 2, requests) + assert.Equal(t, 2, admissions) +} + +func TestVerifyWithRequestGate_AdmitsEvery429RetryAndMetadataSend(t *testing.T) { + requests := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requests++ + w.Header().Set("Content-Type", "application/json") + if requests == 1 { + w.Header().Set("Retry-After", "0") + w.WriteHeader(http.StatusTooManyRequests) + return + } + switch r.URL.Path { + case "/api/v4/user": + _, _ = w.Write([]byte(`{"id":1,"username":"johndoe"}`)) + case "/api/v4/personal_access_tokens/self": + _, _ = w.Write([]byte(`{"active":true,"revoked":false,"scopes":["read_api"]}`)) + default: + http.NotFound(w, r) + } + })) + defer server.Close() + + admissions := 0 + result := (&Verifier{apiURL: server.URL, httpClient: server.Client()}).VerifyWithRequestGate( + context.Background(), + detector.RawFinding{Raw: []byte("glpat-ABCDEFGHIJKLMNOPQRSTUVWXYZab")}, + func() *finding.VerificationResult { admissions++; return nil }, + ) + require.Equal(t, finding.StatusVerifiedActive, result.Status) + assert.Equal(t, 3, requests) + assert.Equal(t, requests, admissions, "each actual HTTP send, including the 429 replay, needs one admission") +} + +func TestDecodeTokenMetadata_GranularScopesAreBoundedAndDeterministic(t *testing.T) { + body := `{ + "active":true,"revoked":false,"scopes":[],"expires_at":"2027-02-03", + "granular_scopes":[ + {"access":"selected_memberships","permissions":["write_job","read_job"],"project_id":6,"group_id":null}, + {"access":"user","permissions":["read_profile"],"project_id":null,"group_id":null} + ] + }` + extra, _, err := decodeTokenMetadata(strings.NewReader(body)) + require.NoError(t, err) + assert.Equal(t, + "granular:selected_memberships:project:6:read_job,granular:selected_memberships:project:6:write_job,granular:user:read_profile", + extra["scopes"]) + assert.Equal(t, "3", extra["scope_count"]) + assert.Equal(t, "2027-02-03", extra["expires_at"]) +} + +func TestVerify_MalformedGranularMetadataDoesNotEraseActiveIdentity(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + if r.URL.Path == "/api/v4/user" { + _, _ = w.Write([]byte(`{"id":1,"username":"johndoe"}`)) + return + } + _, _ = w.Write([]byte(`{"active":true,"revoked":false,"granular_scopes":[{"access":"selected_memberships","permissions":["read_job"],"project_id":null,"group_id":null}]}`)) + })) + defer server.Close() + + result := (&Verifier{apiURL: server.URL, httpClient: server.Client()}).Verify( + context.Background(), detector.RawFinding{Raw: []byte("glpat-ABCDEFGHIJKLMNOPQRSTUVWXYZab")}) + require.Equal(t, finding.StatusVerifiedActive, result.Status) + assert.Equal(t, "johndoe", result.ExtraData["username"]) + assert.NotContains(t, result.ExtraData, "scopes") +} + +func TestVerifyWithRequestGate_RejectionPreventsAnyRequest(t *testing.T) { + called := false + v := &Verifier{apiURL: "https://trusted.example", httpClient: &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) { + called = true + return nil, assert.AnError + })}} + rejection := finding.VerificationResult{Status: finding.StatusVerifyError, Message: "admission rejected"} + result := v.VerifyWithRequestGate(context.Background(), detector.RawFinding{ + Raw: []byte("glpat-ABCDEFGHIJKLMNOPQRSTUVWXYZab"), + }, func() *finding.VerificationResult { return &rejection }) + assert.Equal(t, rejection, result) + assert.False(t, called) } func TestVerify_InvalidToken_ReturnsInactive(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"message":"401 Unauthorized"}`)) })) @@ -66,6 +182,52 @@ func TestVerify_InvalidToken_ReturnsInactive(t *testing.T) { assert.Equal(t, "GitLab token is invalid or revoked", result.Message) } +func TestVerify_DPoPChallengeDoesNotProveTokenInactive(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"invalid_token","error_description":"DPoP proof required"}`)) + })) + defer server.Close() + + result := (&Verifier{apiURL: server.URL, httpClient: server.Client()}).Verify( + context.Background(), + detector.RawFinding{DetectorID: detectorID, Raw: []byte("glpat-ABCDEFGHIJKLMNOPQRSTUVWXYZab")}, + ) + assert.Equal(t, finding.StatusVerifyError, result.Status) + assert.NotContains(t, result.Message, "DPoP") +} + +func TestVerify_ActiveResponseRequiresCompleteJSONIdentity(t *testing.T) { + tests := []struct { + name string + contentType string + body string + }{ + {name: "empty object", contentType: "application/json", body: `{}`}, + {name: "null", contentType: "application/json", body: `null`}, + {name: "missing username", contentType: "application/json", body: `{"id":1}`}, + {name: "missing id", contentType: "application/json", body: `{"username":"johndoe"}`}, + {name: "trailing json", contentType: "application/json", body: `{"id":1,"username":"johndoe"}{}`}, + {name: "wrong content type", contentType: "text/html", body: `{"id":1,"username":"johndoe"}`}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", tc.contentType) + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(tc.body)) + })) + defer server.Close() + result := (&Verifier{apiURL: server.URL, httpClient: server.Client()}).Verify( + context.Background(), + detector.RawFinding{DetectorID: detectorID, Raw: []byte("glpat-ABCDEFGHIJKLMNOPQRSTUVWXYZab")}, + ) + assert.Equal(t, finding.StatusVerifyError, result.Status) + }) + } +} + func TestVerify_ServerError_ReturnsError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) @@ -95,69 +257,42 @@ func TestVerify_Type_ReturnsCorrectID(t *testing.T) { assert.Equal(t, "gitlab-pat", v.Type()) } -func TestBaseURL_DerivesHostFromExtraData(t *testing.T) { - tests := []struct { - name string - apiURL string - extra map[string]string - want string - }{ - { - name: "no context defaults to gitlab.com", - want: "https://gitlab.com", - }, - { - name: "self-hosted host from ExtraData", - extra: map[string]string{"host": "gitlab.example.com"}, - want: "https://gitlab.example.com", - }, - { - name: "self-hosted host with port", - extra: map[string]string{"host": "gitlab.corp.internal:8443"}, - want: "https://gitlab.corp.internal:8443", - }, - { - name: "apiURL override wins", - apiURL: "http://127.0.0.1:1234", - extra: map[string]string{"host": "gitlab.example.com"}, - want: "http://127.0.0.1:1234", - }, +func TestVerify_RepositoryHostNeverSelectsDestination(t *testing.T) { + called := false + v := &Verifier{httpClient: &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) { + called = true + return nil, assert.AnError + })}} + raw := detector.RawFinding{ + DetectorID: detectorID, + Raw: []byte("glpat-selfhostedtoken123456789012"), + ExtraData: map[string]string{"host": "gitlab.attacker.example"}, } + result := v.Verify(context.Background(), raw) + require.Equal(t, finding.StatusUnverified, result.Status) + assert.False(t, called) +} - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - v := &Verifier{apiURL: tt.apiURL} - raw := detector.RawFinding{DetectorID: detectorID, ExtraData: tt.extra} - assert.Equal(t, tt.want, v.baseURL(raw)) +func TestVerify_NonPATSubtypesRemainUnverified(t *testing.T) { + for _, prefix := range []string{ + "gldt-", "glrt-", "glrtr-", "glcbt-", "glptt-", "glimt-", "glagent-", + "glwt-", "glsoat-", "glffct-", "gloas-", "glft-", + } { + result := (&Verifier{apiURL: "https://trusted.example"}).Verify(context.Background(), detector.RawFinding{ + DetectorID: detectorID, Raw: []byte(prefix + "abcDEF1234567890xyzW"), }) + assert.Equal(t, finding.StatusUnverified, result.Status, prefix) } } -func TestVerify_SelfHostedActiveToken_NotReportedInvalid(t *testing.T) { - // A live self-hosted token verified against its true issuer must read as - // active, never as "invalid or revoked". The test server stands in for the - // self-hosted instance the detector captured into ExtraData["host"]. - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/api/v4/user", r.URL.Path) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(`{"username":"selfhosted-user"}`)) - })) - defer server.Close() - - v := &Verifier{apiURL: server.URL, httpClient: server.Client()} - - raw := detector.RawFinding{ - DetectorID: detectorID, - Raw: []byte("glpat-selfhostedtoken123456789012"), - Redacted: "glpat-****9012", - ExtraData: map[string]string{"host": "gitlab.example.com"}, +func TestNewForTrustedInstance_ValidatesOrigin(t *testing.T) { + configured, err := NewForTrustedInstance("https://gitlab.example.com/") + require.NoError(t, err) + assert.Equal(t, "https://gitlab.example.com", configured.apiURL) + for _, origin := range []string{"http://gitlab.com", "https://127.0.0.1", "https://localhost", "https://gitlab.com/path"} { + _, err := NewForTrustedInstance(origin) + assert.Error(t, err, origin) } - - result := v.Verify(context.Background(), raw) - - require.Equal(t, finding.StatusVerifiedActive, result.Status) - assert.Equal(t, "selfhosted-user", result.ExtraData["username"]) } func TestVerify_EmptyToken_ReturnsUnverified(t *testing.T) { @@ -174,3 +309,9 @@ func TestVerify_EmptyToken_ReturnsUnverified(t *testing.T) { assert.Equal(t, finding.StatusUnverified, result.Status) assert.Equal(t, "empty token", result.Message) } + +type roundTripFunc func(*http.Request) (*http.Response, error) + +func (f roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) { + return f(request) +} diff --git a/internal/verifier/grafana/grafana_shared_test.go b/internal/verifier/grafana/grafana_shared_test.go index ec824d4..44fca1c 100644 --- a/internal/verifier/grafana/grafana_shared_test.go +++ b/internal/verifier/grafana/grafana_shared_test.go @@ -13,15 +13,12 @@ func TestVerify_SharedSafetySuite(t *testing.T) { vtest.Run(t, vtest.Case{ Name: "grafana", New: func(apiURL string, client *http.Client) verifier.Verifier { - return &Verifier{apiURL: apiURL, httpClient: client} + return &Verifier{instanceURL: apiURL, httpClient: client} }, Raw: detector.RawFinding{ DetectorID: detectorID, - Raw: []byte("glsa_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef12345678"), + Raw: []byte("glsa_sharedsafetyfixture12345678901234567_12345678"), Redacted: "glsa_****5678", }, - // The Grafana verifier checks only the status code on success; it has - // no Decode func to fail on a malformed body. - SkipMalformed: true, }) } diff --git a/internal/verifier/grafana/grafana_verifier.go b/internal/verifier/grafana/grafana_verifier.go index bf8797b..6c7686b 100644 --- a/internal/verifier/grafana/grafana_verifier.go +++ b/internal/verifier/grafana/grafana_verifier.go @@ -1,10 +1,15 @@ -// Package grafana provides a verifier for Grafana API keys. -// It uses the Grafana HTTP API GET /api/user endpoint to check key validity. +// Package grafana provides a verifier for Grafana service-account tokens. +// It uses the issuing instance's read-only access-control permissions endpoint. package grafana import ( "context" + "encoding/json" + "fmt" + "io" "net/http" + "net/url" + "strings" "github.com/HodeTech/leakwatch/internal/detector" "github.com/HodeTech/leakwatch/internal/verifier" @@ -14,14 +19,16 @@ import ( const detectorID = "grafana-api-key" -// defaultAPIURL is the base URL for the Grafana Cloud API. -const defaultAPIURL = "https://grafana.com" +const permissionsPath = "/api/access-control/user/permissions" -// Verifier checks whether a Grafana API key is active by calling the -// Grafana Cloud API. It NEVER logs or persists raw key values. +// Verifier checks whether a Grafana service-account token is active against +// its issuing instance. It NEVER logs or persists raw token values. type Verifier struct { - // apiURL overrides the Grafana Cloud API base URL (for testing). - apiURL string + // instanceURL is either empty or normalized by NewForTrustedInstance. Tests + // in this package may set it directly to reach a hermetic HTTP server. + // Repository content and RawFinding.ExtraData must never populate it because + // doing so would turn verification into an SSRF primitive. + instanceURL string // httpClient overrides the default HTTP client (for testing). httpClient *http.Client } @@ -30,33 +37,120 @@ func init() { verifier.Register(&Verifier{}) } +// NewForTrustedInstance constructs a Grafana verifier for an instance origin +// explicitly supplied by the operator. Callers MUST NOT pass a URL extracted +// from scanned repository content. The CLI intentionally exposes this only as +// a command-line flag, not as a project-config or finding field. +func NewForTrustedInstance(instanceURL string) (*Verifier, error) { + normalized, err := normalizeTrustedInstanceURL(instanceURL) + if err != nil { + return nil, err + } + return &Verifier{instanceURL: normalized}, nil +} + +// WithTrustedInstance implements verifier.TrustedInstanceConfigurer. +func (*Verifier) WithTrustedInstance(instanceURL string) (verifier.Verifier, error) { + return NewForTrustedInstance(instanceURL) +} + // Type returns the detector ID this verifier handles. func (v *Verifier) Type() string { return detectorID } -// Verify checks if the detected Grafana API key is valid/active. +// Verify checks if the detected Grafana service-account token is valid/active. // Raw contains the key value. // -// This calls GET /api/user, matching docs/architecture/05-VERIFIER-ANALYSIS.md -// and docs/guides/secret-verification.md (the code previously called the -// nonexistent /api/viewer path). Note the caveat those docs already record: -// glsa_ tokens are Grafana Cloud service-account tokens scoped to the stack -// that issued them, so a token minted on a self-hosted or per-stack instance -// will not authenticate against the central grafana.com host used here — -// there is currently no per-instance URL captured for this detector to -// verify against, the same limitation documented for Okta. +// Grafana service-account tokens are scoped to the instance that issued them. +// The detector does not currently have a trusted instance URL, so the +// production-registered verifier deliberately performs no request and returns +// unverified. In particular, it never trusts an arbitrary URL extracted from +// repository content or falls back to the central grafana.com portal. func (v *Verifier) Verify(ctx context.Context, raw detector.RawFinding) finding.VerificationResult { token := string(raw.Raw) - apiURL := httpx.BaseURL(v.apiURL, defaultAPIURL) + if strings.TrimSpace(token) == "" { + return finding.VerificationResult{ + Status: finding.StatusUnverified, + Message: "empty token", + } + } + if v.instanceURL == "" { + return finding.VerificationResult{ + Status: finding.StatusUnverified, + Message: "Grafana instance URL required", + } + } return httpx.VerifyToken(ctx, v.httpClient, token, httpx.TokenSpec{ Name: "grafana", Request: httpx.Request{ - URL: apiURL + "/api/user", - Header: map[string]string{"Authorization": "Bearer " + token}, + URL: v.instanceURL + permissionsPath, + Header: map[string]string{ + "Accept": "application/json", + "Authorization": "Bearer " + token, + }, }, - ActiveMessage: "Grafana API key is active", - InactiveMessage: "Grafana API key is invalid or revoked", + ActiveMessage: "Grafana API key is active", + InactiveMessage: "Grafana API key is invalid or revoked", + Decode: decodePermissionsObject, + DecodeInactive: decodeInactiveResponse, + RequireCompleteBody: true, + RequireJSONContentType: true, }) } + +func decodeInactiveResponse(body io.Reader) error { + var response struct { + Message string `json:"message"` + } + decoder := json.NewDecoder(body) + if err := decoder.Decode(&response); err != nil { + return err + } + switch strings.ToLower(strings.TrimSpace(response.Message)) { + case "invalid api key", "unauthorized": + default: + return fmt.Errorf("unexpected Grafana authentication error") + } + if decoder.Decode(&struct{}{}) != io.EOF { + return fmt.Errorf("unexpected trailing Grafana response data") + } + return nil +} + +func normalizeTrustedInstanceURL(rawURL string) (string, error) { + normalized, err := verifier.NormalizeTrustedHTTPSOrigin(rawURL) + if err != nil { + return "", fmt.Errorf("invalid Grafana instance URL: %w", err) + } + u, err := url.Parse(normalized) + if err != nil { + return "", fmt.Errorf("invalid Grafana instance URL after normalization: %w", err) + } + hostname := u.Hostname() + if hostname == "grafana.com" || hostname == "www.grafana.com" { + return "", fmt.Errorf("invalid Grafana instance URL: the central grafana.com portal is not an issuing instance") + } + return normalized, nil +} + +func decodePermissionsObject(body io.Reader) (map[string]string, string, error) { + decoder := json.NewDecoder(body) + var permissions map[string]json.RawMessage + if err := decoder.Decode(&permissions); err != nil { + return nil, "", fmt.Errorf("invalid Grafana permissions response: %w", err) + } + if permissions == nil { + return nil, "", fmt.Errorf("invalid Grafana permissions response: expected JSON object") + } + + var trailing json.RawMessage + if err := decoder.Decode(&trailing); err != io.EOF { + if err == nil { + return nil, "", fmt.Errorf("invalid Grafana permissions response: trailing JSON value") + } + return nil, "", fmt.Errorf("invalid Grafana permissions response: %w", err) + } + return nil, "", nil +} diff --git a/internal/verifier/grafana/grafana_verifier_test.go b/internal/verifier/grafana/grafana_verifier_test.go index d0e65b6..6bf7239 100644 --- a/internal/verifier/grafana/grafana_verifier_test.go +++ b/internal/verifier/grafana/grafana_verifier_test.go @@ -2,8 +2,10 @@ package grafana import ( "context" + "fmt" "net/http" "net/http/httptest" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -15,8 +17,10 @@ import ( func TestVerify_ValidKey_ReturnsActive(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/api/user", r.URL.Path) - assert.Contains(t, r.Header.Get("Authorization"), "Bearer ") + assert.Equal(t, permissionsPath, r.URL.Path) + assert.Equal(t, http.MethodGet, r.Method) + assert.Equal(t, "application/json", r.Header.Get("Accept")) + assert.Equal(t, "Bearer glsa_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef12345678", r.Header.Get("Authorization")) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) @@ -25,8 +29,8 @@ func TestVerify_ValidKey_ReturnsActive(t *testing.T) { defer server.Close() v := &Verifier{ - apiURL: server.URL, - httpClient: server.Client(), + instanceURL: strings.TrimRight(server.URL, "/"), + httpClient: server.Client(), } raw := detector.RawFinding{ @@ -43,14 +47,15 @@ func TestVerify_ValidKey_ReturnsActive(t *testing.T) { func TestVerify_InvalidKey_ReturnsInactive(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"message":"Invalid API key"}`)) })) defer server.Close() v := &Verifier{ - apiURL: server.URL, - httpClient: server.Client(), + instanceURL: server.URL, + httpClient: server.Client(), } raw := detector.RawFinding{ @@ -65,6 +70,25 @@ func TestVerify_InvalidKey_ReturnsInactive(t *testing.T) { assert.Equal(t, "Grafana API key is invalid or revoked", result.Message) } +func TestVerify_InactiveResponseMustBeDefinitiveJSON(t *testing.T) { + tests := []struct{ contentType, body string }{ + {contentType: "text/html", body: `login`}, + {contentType: "application/json", body: `{"message":"WAF challenge"}`}, + } + for _, tc := range tests { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", tc.contentType) + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(tc.body)) + })) + result := (&Verifier{instanceURL: server.URL, httpClient: server.Client()}).Verify( + context.Background(), detector.RawFinding{Raw: []byte("glsa_synthetic123456789012345678901234")}, + ) + server.Close() + assert.Equal(t, finding.StatusVerifyError, result.Status) + } +} + func TestVerify_ServerError_ReturnsError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) @@ -73,8 +97,8 @@ func TestVerify_ServerError_ReturnsError(t *testing.T) { defer server.Close() v := &Verifier{ - apiURL: server.URL, - httpClient: server.Client(), + instanceURL: server.URL, + httpClient: server.Client(), } raw := detector.RawFinding{ @@ -89,6 +113,24 @@ func TestVerify_ServerError_ReturnsError(t *testing.T) { assert.Contains(t, result.Message, "500") } +func TestVerify_ForbiddenIsNotInactive(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusForbidden) + })) + defer server.Close() + + v := &Verifier{instanceURL: server.URL, httpClient: server.Client()} + raw := detector.RawFinding{ + DetectorID: detectorID, + Raw: []byte("glsa_permissionlimited12345678901234567890_12345678"), + Redacted: "glsa_****5678", + } + + result := v.Verify(context.Background(), raw) + assert.Equal(t, finding.StatusVerifyError, result.Status) + assert.Contains(t, result.Message, "403") +} + func TestVerify_Type_ReturnsCorrectID(t *testing.T) { v := &Verifier{} assert.Equal(t, "grafana-api-key", v.Type()) @@ -108,3 +150,160 @@ func TestVerify_EmptyToken_ReturnsUnverified(t *testing.T) { assert.Equal(t, finding.StatusUnverified, result.Status) assert.Equal(t, "empty token", result.Message) } + +func TestVerify_WhitespaceToken_ReturnsUnverifiedWithoutRequest(t *testing.T) { + requests := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + requests++ + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + v := &Verifier{instanceURL: server.URL, httpClient: server.Client()} + result := v.Verify(context.Background(), detector.RawFinding{Raw: []byte(" \t\r\n")}) + + assert.Equal(t, finding.StatusUnverified, result.Status) + assert.Equal(t, "empty token", result.Message) + assert.Zero(t, requests) +} + +func TestVerify_NoTrustedInstance_ReturnsUnverifiedWithoutRequest(t *testing.T) { + requests := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + requests++ + w.WriteHeader(http.StatusUnauthorized) + })) + defer server.Close() + + for _, field := range []string{"instance_url", "grafana_url", "url", "domain"} { + t.Run(field, func(t *testing.T) { + v := &Verifier{httpClient: server.Client()} + raw := detector.RawFinding{ + DetectorID: detectorID, + Raw: []byte("glsa_repositorycontrolled123456789012345_12345678"), + Redacted: "glsa_****5678", + ExtraData: map[string]string{field: server.URL}, + } + + result := v.Verify(context.Background(), raw) + assert.Equal(t, finding.StatusUnverified, result.Status) + assert.Equal(t, "Grafana instance URL required", result.Message) + }) + } + assert.Zero(t, requests, "repository-provided URLs must never become verifier targets") +} + +func TestVerify_CentralPortalHintCannotProduceInactive(t *testing.T) { + v := &Verifier{} + raw := detector.RawFinding{ + DetectorID: detectorID, + Raw: []byte("glsa_centralportalfixture123456789012345_12345678"), + Redacted: "glsa_****5678", + ExtraData: map[string]string{"instance_url": "https://grafana.com"}, + } + + result := v.Verify(context.Background(), raw) + assert.Equal(t, finding.StatusUnverified, result.Status) + assert.NotEqual(t, finding.StatusVerifiedInactive, result.Status) +} + +func TestVerify_RedirectIsNotFollowedOrInactive(t *testing.T) { + targetRequests := 0 + target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + targetRequests++ + w.WriteHeader(http.StatusUnauthorized) + })) + defer target.Close() + + issuer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, target.URL, http.StatusFound) + })) + defer issuer.Close() + v := &Verifier{instanceURL: issuer.URL} + raw := detector.RawFinding{ + DetectorID: detectorID, + Raw: []byte("glsa_redirectfixture123456789012345678901_12345678"), + Redacted: "glsa_****5678", + } + result := v.Verify(context.Background(), raw) + + assert.Equal(t, finding.StatusVerifyError, result.Status) + assert.Zero(t, targetRequests) +} + +func TestVerify_InvalidSuccessBodiesAreNotActive(t *testing.T) { + tests := []struct { + name string + contentType string + body string + }{ + {name: "HTML login page", contentType: "text/html", body: "Sign in"}, + {name: "HTML content type with JSON object", contentType: "text/html", body: `{}`}, + {name: "plain content type with JSON object", contentType: "text/plain", body: `{}`}, + {name: "missing content type with JSON object", body: `{}`}, + {name: "malformed JSON", contentType: "application/json", body: `{not-json`}, + {name: "null", contentType: "application/json", body: `null`}, + {name: "array", contentType: "application/json", body: `[]`}, + {name: "scalar", contentType: "application/json", body: `true`}, + {name: "trailing value", contentType: "application/json", body: `{} {}`}, + {name: "truncated oversized object", contentType: "application/json", body: `{"permission":"` + strings.Repeat("x", 1<<20) + `"}`}, + {name: "oversized trailing whitespace", contentType: "application/json", body: `{}` + strings.Repeat(" ", (1<<20)+1)}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", tc.contentType) + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(tc.body)) + })) + defer server.Close() + + v := &Verifier{instanceURL: server.URL, httpClient: server.Client()} + result := v.Verify(context.Background(), detector.RawFinding{ + Raw: []byte("glsa_invalidbodyfixture12345678901234567_12345678"), + Redacted: "glsa_****5678", + }) + + assert.Equal(t, finding.StatusVerifyError, result.Status) + assert.NotEqual(t, finding.StatusVerifiedActive, result.Status) + }) + } +} + +func TestNewForTrustedInstance_Validation(t *testing.T) { + tests := []struct { + name string + url string + want string + }{ + {name: "Grafana Cloud stack", url: " https://example.grafana.net/ ", want: "https://example.grafana.net"}, + {name: "self hosted origin", url: "https://grafana.example.test:8443", want: "https://grafana.example.test:8443"}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + v, err := NewForTrustedInstance(tc.url) + require.NoError(t, err) + assert.Equal(t, tc.want, v.instanceURL) + }) + } + + invalid := []string{ + "", "grafana.example.test", "http://grafana.example.test", + "https://user:pass@grafana.example.test", "https://grafana.example.test/path", + "https://grafana.example.test?next=x", "https://grafana.example.test#fragment", + "https://grafana.com", "https://www.grafana.com/", "https://localhost", + "https://grafana.localhost", "https://127.0.0.1", "https://10.0.0.1", + "https://169.254.169.254", "https://[::1]", "https://*.grafana.net", + "https://[fe80::1%25en0]", "https://[::ffff:127.0.0.1]", + "https://127.1", "https://2130706433", "https://017700000001", "https://0x7f000001", + "https://8.8.8.8", "https://[2001:4860:4860::8888]", + } + for i, rawURL := range invalid { + t.Run(fmt.Sprintf("invalid-%d", i), func(t *testing.T) { + v, err := NewForTrustedInstance(rawURL) + require.Error(t, err) + assert.Nil(t, v) + }) + } +} diff --git a/internal/verifier/heroku/heroku_shared_test.go b/internal/verifier/heroku/heroku_shared_test.go new file mode 100644 index 0000000..4b63a91 --- /dev/null +++ b/internal/verifier/heroku/heroku_shared_test.go @@ -0,0 +1,16 @@ +package heroku + +import ( + "net/http" + "testing" + + "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/internal/verifier" + "github.com/HodeTech/leakwatch/internal/verifier/internal/vtest" +) + +func TestVerify_SharedSafetySuite(t *testing.T) { + vtest.Run(t, vtest.Case{Name: "heroku", New: func(url string, client *http.Client) verifier.Verifier { + return &Verifier{apiURL: url, httpClient: client} + }, Raw: detector.RawFinding{DetectorID: detectorID, Raw: []byte("12345678-1234-abcd-9876-1234567890ab")}}) +} diff --git a/internal/verifier/http_contract_coverage_test.go b/internal/verifier/http_contract_coverage_test.go new file mode 100644 index 0000000..3b02155 --- /dev/null +++ b/internal/verifier/http_contract_coverage_test.go @@ -0,0 +1,178 @@ +package verifier_test + +import ( + "go/ast" + "go/build" + "go/parser" + "go/token" + "path/filepath" + "sort" + "strconv" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var expectedHTTPVerifierPackages = []string{ + "airtable", "anthropic", "auth0", "bitbucket", "circleci", "cloudflare", + "databricks", "datadog", "deepseek", "digitalocean", "discord", "dockerhub", + "doppler", "figma", "github", "gitlab", "grafana", "heroku", "huggingface", + "infura", "launchdarkly", "linear", "mailgun", "newrelic", "notion", "npm", + "okta", "openai", "pagerduty", "postmark", "pypi", "rubygems", "sendgrid", + "sentry", "shopify", "slack", "snyk", "sonarcloud", "stripe", "supabase", + "teams", "telegram", "terraform", "twilio", "vercel", +} + +// TestHTTPVerifierPackages_UseSharedSafetySuite ties network discovery to exact +// package identities and to a directly executed top-level vtest call. A raw +// comment marker, an aliased import, a helper hidden in a non-verifier filename, +// or a dead nested function cannot satisfy the guard. +func TestHTTPVerifierPackages_UseSharedSafetySuite(t *testing.T) { + entries, err := filepath.Glob("*") + require.NoError(t, err) + discovered := make([]string, 0, len(expectedHTTPVerifierPackages)) + for _, entry := range entries { + info, statErr := filepath.Glob(filepath.Join(entry, "*.go")) + require.NoError(t, statErr) + if len(info) == 0 || entry == "internal" || !packageUsesHTTP(t, entry) { + continue + } + discovered = append(discovered, entry) + assert.True(t, packageRunsVTestDirectly(t, entry), + "HTTP verifier package %q must execute vtest.Run as the first statement of a top-level Test function", entry) + } + sort.Strings(discovered) + assert.Equal(t, expectedHTTPVerifierPackages, discovered, + "HTTP verifier package identities changed; add/remove an executable safety contract deliberately") +} + +func packageUsesHTTP(t *testing.T, directory string) bool { + t.Helper() + paths := activeGoFiles(t, directory, false) + for _, path := range paths { + file := parseGoFile(t, path) + httpxAliases := importAliases(file, "github.com/HodeTech/leakwatch/internal/verifier/internal/httpx", "httpx") + httpAliases := importAliases(file, "net/http", "http") + importsHTTP := len(httpAliases) > 0 + found := false + ast.Inspect(file, func(node ast.Node) bool { + call, ok := node.(*ast.CallExpr) + if !ok { + return true + } + selector, ok := call.Fun.(*ast.SelectorExpr) + if !ok { + return true + } + receiver, _ := selector.X.(*ast.Ident) + if receiver != nil && httpxAliases[receiver.Name] && selector.Sel.Name == "VerifyToken" { + found = true + return false + } + if receiver != nil && httpAliases[receiver.Name] && (selector.Sel.Name == "NewRequestWithContext" || selector.Sel.Name == "NewRequest") { + found = true + return false + } + // Direct clients and transports may be stored behind a local field or + // interface, so their receiver is not the net/http import identifier. + // When the production file imports net/http, recognize the outbound + // method surface as network use as well. + if importsHTTP && (selector.Sel.Name == "Do" || selector.Sel.Name == "RoundTrip" || + selector.Sel.Name == "Get" || selector.Sel.Name == "Post" || selector.Sel.Name == "PostForm" || + selector.Sel.Name == "Head") { + found = true + return false + } + return true + }) + if found { + return true + } + } + return false +} + +func packageRunsVTestDirectly(t *testing.T, directory string) bool { + t.Helper() + paths := activeGoFiles(t, directory, true) + for _, path := range paths { + file := parseGoFile(t, path) + aliases := importAliases(file, "github.com/HodeTech/leakwatch/internal/verifier/internal/vtest", "vtest") + if len(aliases) == 0 { + continue + } + for _, declaration := range file.Decls { + function, ok := declaration.(*ast.FuncDecl) + if !ok || function.Recv != nil || !strings.HasPrefix(function.Name.Name, "Test") || + function.Body == nil || len(function.Body.List) == 0 { + continue + } + expression, ok := function.Body.List[0].(*ast.ExprStmt) + if !ok { + continue + } + call, ok := expression.X.(*ast.CallExpr) + if !ok { + continue + } + selector, ok := call.Fun.(*ast.SelectorExpr) + if !ok || selector.Sel.Name != "Run" { + continue + } + receiver, ok := selector.X.(*ast.Ident) + if ok && aliases[receiver.Name] { + return true + } + } + } + return false +} + +func activeGoFiles(t *testing.T, directory string, tests bool) []string { + t.Helper() + pattern := "*.go" + if tests { + pattern = "*_test.go" + } + paths, err := filepath.Glob(filepath.Join(directory, pattern)) + require.NoError(t, err) + active := make([]string, 0, len(paths)) + for _, path := range paths { + if !tests && strings.HasSuffix(path, "_test.go") { + continue + } + matched, matchErr := build.Default.MatchFile(directory, filepath.Base(path)) + require.NoError(t, matchErr, path) + if matched { + active = append(active, path) + } + } + return active +} + +func importAliases(file *ast.File, importPath, defaultName string) map[string]bool { + aliases := make(map[string]bool) + for _, spec := range file.Imports { + path, err := strconv.Unquote(spec.Path.Value) + if err != nil || path != importPath { + continue + } + name := defaultName + if spec.Name != nil { + name = spec.Name.Name + } + if name != "." && name != "_" { + aliases[name] = true + } + } + return aliases +} + +func parseGoFile(t *testing.T, path string) *ast.File { + t.Helper() + file, err := parser.ParseFile(token.NewFileSet(), path, nil, 0) + require.NoError(t, err) + return file +} diff --git a/internal/verifier/huggingface/huggingface_shared_test.go b/internal/verifier/huggingface/huggingface_shared_test.go new file mode 100644 index 0000000..66efe71 --- /dev/null +++ b/internal/verifier/huggingface/huggingface_shared_test.go @@ -0,0 +1,16 @@ +package huggingface + +import ( + "net/http" + "testing" + + "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/internal/verifier" + "github.com/HodeTech/leakwatch/internal/verifier/internal/vtest" +) + +func TestVerify_SharedSafetySuite(t *testing.T) { + vtest.Run(t, vtest.Case{Name: "huggingface", New: func(url string, client *http.Client) verifier.Verifier { + return &Verifier{apiURL: url, httpClient: client} + }, Raw: detector.RawFinding{DetectorID: detectorID, Raw: []byte("hf_SyntheticHuggingFaceCredentialFixture1234")}}) +} diff --git a/internal/verifier/infura/infura_shared_test.go b/internal/verifier/infura/infura_shared_test.go new file mode 100644 index 0000000..a8afe0f --- /dev/null +++ b/internal/verifier/infura/infura_shared_test.go @@ -0,0 +1,16 @@ +package infura + +import ( + "net/http" + "testing" + + "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/internal/verifier" + "github.com/HodeTech/leakwatch/internal/verifier/internal/vtest" +) + +func TestVerify_SharedSafetySuite(t *testing.T) { + vtest.Run(t, vtest.Case{Name: "infura", New: func(url string, client *http.Client) verifier.Verifier { + return &Verifier{apiURL: url, httpClient: client} + }, Raw: detector.RawFinding{DetectorID: detectorID, Raw: []byte("abcdef0123456789abcdef0123456789")}}) +} diff --git a/internal/verifier/internal/httpx/httpx.go b/internal/verifier/internal/httpx/httpx.go index a8e703a..9dc1fb7 100644 --- a/internal/verifier/internal/httpx/httpx.go +++ b/internal/verifier/internal/httpx/httpx.go @@ -29,9 +29,12 @@ // relying on the crypto/tls default, as defense-in-depth and // self-documentation. // -// This helper deliberately does NOT implement retry, backoff, or per-provider -// rate limiting. Those concerns are handled (or deferred) elsewhere; keeping -// this package focused on transport safety. +// The shared token helper performs at most one Retry-After-aware HTTP 429 retry +// for safe GET/HEAD probes. The wait is strictly bounded, context-aware, and +// passes through an engine-owned admission gate immediately before every actual +// send when verification runs through the engine. Format-only, empty-input, and +// missing-context paths consume no limiter capacity. Unsafe methods and +// missing/invalid Retry-After responses are never retried. package httpx import ( @@ -129,7 +132,10 @@ func RedactError(err error, secret string) string { if err == nil { return "" } - msg := err.Error() + return redactText(err.Error(), secret) +} + +func redactText(msg, secret string) string { if secret == "" { return msg } diff --git a/internal/verifier/internal/httpx/verify.go b/internal/verifier/internal/httpx/verify.go index 48001ce..b0d29a7 100644 --- a/internal/verifier/internal/httpx/verify.go +++ b/internal/verifier/internal/httpx/verify.go @@ -3,10 +3,17 @@ package httpx import ( "bytes" "context" + cryptorand "crypto/rand" + "encoding/base64" + "encoding/binary" "fmt" "io" "log/slog" + "mime" "net/http" + "strconv" + "strings" + "time" "github.com/HodeTech/leakwatch/pkg/finding" ) @@ -14,9 +21,49 @@ import ( // userAgent is the User-Agent every verifier request carries. const userAgent = "leakwatch-verifier" -// Request describes the single HTTP request a verifier sends to a provider. +const ( + max429Attempts = 2 + max429RetryWait = 2 * time.Second + maxJitterRatio = 0.10 +) + +type retryGateKey struct{} + +type requestGateKey struct{} + +// RequestGate is installed by the verification engine for standard httpx +// verifiers. It admits each actual send through both provider and global rate +// limiters. Returning a non-nil result rejects the request before client.Do. +type RequestGate func() *finding.VerificationResult + +// WithRequestGate returns a child context carrying the engine-owned admission +// gate used immediately before every HTTP attempt, including a bounded retry. +func WithRequestGate(ctx context.Context, gate RequestGate) context.Context { + if gate == nil { + return ctx + } + return context.WithValue(ctx, requestGateKey{}, gate) +} + +// RetryGate is installed by the verification engine and admits one retry at +// its actual send point through both the provider and global rate limiters. +// Returning a non-nil result rejects the retry without sending it. +type RetryGate func() *finding.VerificationResult + +// WithRetryGate returns a child context carrying an engine-owned admission gate +// used only for an HTTP 429 retry. It supports manually gated multi-request +// verifiers whose initial/fallback sends call their verifier.RequestGate +// directly; standard httpx verifiers use WithRequestGate instead. +func WithRetryGate(ctx context.Context, gate RetryGate) context.Context { + if gate == nil { + return ctx + } + return context.WithValue(ctx, retryGateKey{}, gate) +} + +// Request describes the logical HTTP probe a verifier sends to a provider. // VerifyToken builds and performs it through the shared, security-hardened -// client, so callers only declare what differs between providers. +// client; a safe GET/HEAD probe may be replayed once after a bounded HTTP 429. type Request struct { // Method defaults to GET when empty. Method string @@ -42,8 +89,21 @@ type Request struct { // The reader passed to a DecodeFunc is already bounded by LimitReader. type DecodeFunc func(body io.Reader) (extra map[string]string, downgradeMessage string, err error) -// TokenSpec describes a standard single-request token verification: the request -// to send and how each response status maps to a VerificationResult. +// ResponseDecodeFunc is the header-aware counterpart to DecodeFunc. It is for +// provider metadata that is available only in response headers (for example +// GitHub OAuth scopes and token expiry). Header values are untrusted provider +// input and must never be copied wholesale into ExtraData. +type ResponseDecodeFunc func(header http.Header, body io.Reader) (extra map[string]string, downgradeMessage string, err error) + +// InactiveDecodeFunc validates that an inactive-status response is definitive +// for the provider. A nil error permits verified-inactive; an error keeps the +// outcome fail-conservative as StatusVerifyError. It is intended for providers +// whose 401 class also includes challenges such as DPoP that do not prove a +// credential is invalid. +type InactiveDecodeFunc func(body io.Reader) error + +// TokenSpec describes a standard token verification probe: the request to send +// and how each response status maps to a VerificationResult. // // The shared flow — User-Agent, no-redirect handling, bounded body, error // redaction, and the canonical "unexpected status code" / "failed to decode" @@ -57,9 +117,9 @@ type TokenSpec struct { // Request is the provider request to send. Request Request - // Redact, when non-empty, is stripped from any error text before it is - // logged or returned. Set it to the secret when the credential appears in - // the request URL (token-in-path verifiers such as telegram and infura). + // Redact is an optional additional sensitive value stripped from error text. + // VerifyToken always redacts its token argument; use this field only when a + // request contains another credential-bearing representation. Redact string // ActiveStatuses are the HTTP status codes mapped to verified-active. @@ -86,6 +146,26 @@ type TokenSpec struct { // extract ExtraData (and optionally downgrade the result). When nil, an // active-status response yields a bare active result without reading the body. Decode DecodeFunc + + // DecodeResponse is mutually exclusive with Decode and receives both the + // active response headers and bounded body. Use it only for explicitly + // validated, non-secret provider metadata. + DecodeResponse ResponseDecodeFunc + + // DecodeInactive, when non-nil, must positively validate an inactive-status + // response body before the credential is classified as inactive. The body is + // read completely through the same strict size bound used for active bodies. + DecodeInactive InactiveDecodeFunc + + // RequireCompleteBody reads the full active response through a strict + // MaxBodyBytes+1 bound before Decode runs. Responses over the bound are + // rejected instead of letting a truncated prefix appear valid. + RequireCompleteBody bool + + // RequireJSONContentType rejects an active response unless its media type is + // application/json or an application/*+json subtype. The raw header is never + // reflected into logs or result messages. + RequireJSONContentType bool } // BaseURL returns override when it is non-empty, otherwise fallback. Verifiers @@ -109,29 +189,71 @@ func VerifyToken(ctx context.Context, client *http.Client, token string, spec To } } - resp, errResult := spec.send(ctx, client) - if errResult != nil { - return *errResult + for attempt := 1; attempt <= max429Attempts; attempt++ { + resp, errResult := spec.send(ctx, client, token) + if errResult != nil { + return *errResult + } + + if resp.StatusCode == http.StatusTooManyRequests && attempt < max429Attempts && spec.retrySafe() { + rawRetryAfter := resp.Header.Get("Retry-After") + delay, ok := retryAfterDelay(rawRetryAfter, time.Now()) + closeResponse(resp) + if ok && delay <= max429RetryWait { + delay = addRetryJitter(delay, max429RetryWait, retryJitterUnit()) + if retryFitsContext(ctx, delay) { + slog.DebugContext(ctx, spec.Name+" verifier: scheduling bounded HTTP 429 retry", + slog.Int("next_attempt", attempt+1), + slog.Int("max_attempts", max429Attempts), + slog.Duration("retry_wait", delay), + slog.Duration("max_total_wait", max429RetryWait), + ) + if result := waitForRetry(ctx, spec.Name, delay); result != nil { + return *result + } + if gate, _ := ctx.Value(retryGateKey{}).(RetryGate); gate != nil { + if rejection := gate(); rejection != nil { + return *rejection + } + } + continue + } + } + slog.DebugContext(ctx, spec.Name+" verifier: bounded HTTP 429 retry skipped", + slog.Int("attempt", attempt), + slog.Int("max_attempts", max429Attempts), + slog.Duration("max_total_wait", max429RetryWait), + ) + return RateLimited(ctx, spec.Name, rawRetryAfter) + } + + return func() finding.VerificationResult { + // Decode callbacks are provider-specific code. Preserve response-body + // cleanup even if one panics and the outer verification engine recovers. + defer closeResponse(resp) + return spec.handleResponse(ctx, resp) + }() + } + + return finding.VerificationResult{ + Status: finding.StatusVerifyError, + Message: "bounded verification attempts exhausted", } - defer func() { - // Drain the remaining body (bounded by MaxBodyBytes so a misbehaving - // endpoint cannot exhaust memory) before closing, so the underlying - // connection can be returned to the shared client's keep-alive pool - // instead of being discarded. - _, _ = io.Copy(io.Discard, LimitReader(resp.Body)) - _ = resp.Body.Close() - }() +} +func (spec TokenSpec) handleResponse(ctx context.Context, resp *http.Response) finding.VerificationResult { code := resp.StatusCode switch { case containsStatus(spec.activeStatuses(), code): - return spec.handleActive(ctx, resp.Body) - case containsStatus(spec.inactiveStatuses(), code): - slog.DebugContext(ctx, spec.Name+" verifier: secret is inactive") - return finding.VerificationResult{ - Status: finding.StatusVerifiedInactive, - Message: spec.InactiveMessage, + if spec.RequireJSONContentType && !isJSONContentType(resp.Header.Get("Content-Type")) { + return finding.VerificationResult{ + Status: finding.StatusVerifyError, + Message: "200 OK but response Content-Type is not JSON", + } } + return spec.handleActive(ctx, resp) + case containsStatus(spec.inactiveStatuses(), code): + return spec.handleInactive(ctx, resp) case code == http.StatusTooManyRequests: // A provider-side rate limit must be distinguishable from a genuine // verification bug: report an actionable message rather than a generic @@ -142,10 +264,114 @@ func VerifyToken(ctx context.Context, client *http.Client, token string, spec To } } +func (spec TokenSpec) retrySafe() bool { + method := strings.ToUpper(strings.TrimSpace(spec.Request.Method)) + if method == "" { + method = http.MethodGet + } + return method == http.MethodGet || method == http.MethodHead +} + +func retryAfterDelay(raw string, now time.Time) (time.Duration, bool) { + value := strings.TrimSpace(raw) + if value == "" { + return 0, false + } + if seconds, err := strconv.ParseUint(value, 10, 64); err == nil { + if seconds > uint64(max429RetryWait/time.Second) { + return max429RetryWait + time.Nanosecond, true + } + return time.Duration(seconds) * time.Second, true + } + retryAt, err := http.ParseTime(value) + if err != nil { + return 0, false + } + delay := retryAt.Sub(now) + if delay < 0 { + delay = 0 + } + return delay, true +} + +func addRetryJitter(base, maxDelay time.Duration, unit float64) time.Duration { + if base <= 0 || maxDelay <= base { + return base + } + if unit < 0 { + unit = 0 + } + if unit > 1 { + unit = 1 + } + jitter := time.Duration(float64(base) * maxJitterRatio * unit) + if base+jitter > maxDelay { + return maxDelay + } + return base + jitter +} + +func retryJitterUnit() float64 { + var random [8]byte + if _, err := cryptorand.Read(random[:]); err != nil { + // Jitter improves herd behavior but is not required for correctness. A + // secure-random failure therefore falls back to the provider's exact wait + // instead of introducing a weak pseudo-random source. + return 0 + } + const precisionBits = 53 + value := binary.LittleEndian.Uint64(random[:]) >> (64 - precisionBits) + return float64(value) / float64((uint64(1)< MaxBodyBytes { + return finding.VerificationResult{ + Status: finding.StatusVerifyError, + Message: fmt.Sprintf("200 OK but response body exceeds %d bytes", MaxBodyBytes), + } + } + decodeBody = bytes.NewReader(contents) + } + + var extra map[string]string + var downgrade string + var err error + if spec.DecodeResponse != nil { + extra, downgrade, err = spec.DecodeResponse(resp.Header.Clone(), decodeBody) + } else { + extra, downgrade, err = spec.Decode(decodeBody) + } if err != nil { slog.ErrorContext(ctx, spec.Name+" verifier: failed to decode response", slog.String("error", err.Error())) return finding.VerificationResult{ @@ -238,12 +522,50 @@ func (spec TokenSpec) handleActive(ctx context.Context, body io.Reader) finding. } } +func (spec TokenSpec) handleInactive(ctx context.Context, resp *http.Response) finding.VerificationResult { + if spec.RequireJSONContentType && !isJSONContentType(resp.Header.Get("Content-Type")) { + return finding.VerificationResult{ + Status: finding.StatusVerifyError, + Message: fmt.Sprintf("HTTP %d inactive response Content-Type is not JSON", resp.StatusCode), + } + } + if spec.DecodeInactive != nil { + contents, err := io.ReadAll(io.LimitReader(resp.Body, MaxBodyBytes+1)) + if err != nil { + return finding.VerificationResult{ + Status: finding.StatusVerifyError, + Message: fmt.Sprintf("HTTP %d but failed to read response body: %v", resp.StatusCode, err), + } + } + if int64(len(contents)) > MaxBodyBytes { + return finding.VerificationResult{ + Status: finding.StatusVerifyError, + Message: fmt.Sprintf("HTTP %d but response body exceeds %d bytes", resp.StatusCode, MaxBodyBytes), + } + } + if err := spec.DecodeInactive(bytes.NewReader(contents)); err != nil { + slog.DebugContext(ctx, spec.Name+" verifier: inactive response was not definitive") + return finding.VerificationResult{ + Status: finding.StatusVerifyError, + Message: fmt.Sprintf("HTTP %d did not definitively prove the secret inactive", resp.StatusCode), + } + } + } + + slog.DebugContext(ctx, spec.Name+" verifier: secret is inactive") + return finding.VerificationResult{ + Status: finding.StatusVerifiedInactive, + Message: spec.InactiveMessage, + } +} + // RateLimited returns a distinguished StatusVerifyError result for an HTTP 429 // (Too Many Requests) response, so a provider-side rate limit is never conflated -// with a genuine verification bug or an inactive secret. retryAfter is the raw -// Retry-After header value (may be empty); it is echoed into the message to make -// the outcome actionable and never contains secret material. +// with a genuine verification bug or an inactive secret. Only a syntactically +// valid delta-seconds or HTTP-date Retry-After value is emitted; arbitrary +// provider-controlled header text is never copied into logs or results. func RateLimited(ctx context.Context, name, retryAfter string) finding.VerificationResult { + retryAfter = canonicalRetryAfter(retryAfter) msg := "rate limited by provider (HTTP 429), retry later" if retryAfter != "" { msg = fmt.Sprintf("%s (Retry-After: %s)", msg, retryAfter) @@ -256,6 +578,20 @@ func RateLimited(ctx context.Context, name, retryAfter string) finding.Verificat } } +func canonicalRetryAfter(raw string) string { + value := strings.TrimSpace(raw) + if value == "" { + return "" + } + if seconds, err := strconv.ParseUint(value, 10, 64); err == nil { + return strconv.FormatUint(seconds, 10) + } + if retryAt, err := http.ParseTime(value); err == nil { + return retryAt.UTC().Format(http.TimeFormat) + } + return "" +} + // UnexpectedStatus returns the canonical StatusVerifyError result for a response // status code that a verifier does not recognize. func UnexpectedStatus(ctx context.Context, name string, code int) finding.VerificationResult { diff --git a/internal/verifier/internal/httpx/verify_test.go b/internal/verifier/internal/httpx/verify_test.go index 5fcd966..a5d98d2 100644 --- a/internal/verifier/internal/httpx/verify_test.go +++ b/internal/verifier/internal/httpx/verify_test.go @@ -1,21 +1,45 @@ package httpx import ( + "bytes" "context" + "encoding/base64" "encoding/json" "errors" + "fmt" "io" + "log/slog" "net/http" "net/http/httptest" + "strings" + "sync/atomic" "testing" + "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/HodeTech/leakwatch/pkg/finding" ) const testToken = "test-token-1234567890" +type roundTripFunc func(*http.Request) (*http.Response, error) + +func (f roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) { + return f(request) +} + +type closeTrackingBody struct { + io.Reader + closed *atomic.Bool +} + +func (b *closeTrackingBody) Close() error { + b.closed.Store(true) + return nil +} + // jsonServer returns a test server that responds with the given status code and // body, asserting nothing about the request. func jsonServer(t *testing.T, status int, body string) *httptest.Server { @@ -50,6 +74,40 @@ func TestVerifyToken_Active_NoDecode_NoExtra(t *testing.T) { assert.Nil(t, res.ExtraData) } +func TestVerifyToken_RequiredJSONContentType(t *testing.T) { + tests := []struct { + contentType string + want finding.VerificationStatus + }{ + {contentType: "application/json", want: finding.StatusVerifiedActive}, + {contentType: "application/json; charset=utf-8", want: finding.StatusVerifiedActive}, + {contentType: "application/problem+json", want: finding.StatusVerifiedActive}, + {contentType: "text/html", want: finding.StatusVerifyError}, + {contentType: "", want: finding.StatusVerifyError}, + {contentType: "not a media type", want: finding.StatusVerifyError}, + } + for _, tc := range tests { + t.Run(tc.contentType, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + if tc.contentType != "" { + w.Header().Set("Content-Type", tc.contentType) + } + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{}`)) + })) + defer server.Close() + + result := VerifyToken(context.Background(), server.Client(), testToken, TokenSpec{ + Name: "x", + Request: Request{URL: server.URL}, + ActiveMessage: "active", + RequireJSONContentType: true, + }) + assert.Equal(t, tc.want, result.Status) + }) + } +} + func TestVerifyToken_Active_NoDecode_WithActiveExtra(t *testing.T) { server := jsonServer(t, http.StatusOK, `{}`) defer server.Close() @@ -86,6 +144,45 @@ func TestVerifyToken_Active_Decode_PopulatesExtra(t *testing.T) { assert.Equal(t, "alice", res.ExtraData["name"]) } +func TestVerifyToken_ResponseDecoderReceivesHeadersAndBoundedBody(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("X-Safe-Metadata", "scope-a") + _, _ = w.Write([]byte(`{"ok":true}`)) + })) + defer server.Close() + + result := VerifyToken(context.Background(), server.Client(), testToken, TokenSpec{ + Name: "header-aware", + Request: Request{URL: server.URL}, + DecodeResponse: func(header http.Header, body io.Reader) (map[string]string, string, error) { + contents, err := io.ReadAll(body) + require.NoError(t, err) + assert.JSONEq(t, `{"ok":true}`, string(contents)) + return map[string]string{"scope": header.Get("X-Safe-Metadata")}, "", nil + }, + RequireCompleteBody: true, + RequireJSONContentType: true, + }) + + require.Equal(t, finding.StatusVerifiedActive, result.Status) + assert.Equal(t, "scope-a", result.ExtraData["scope"]) +} + +func TestVerifyToken_RejectsMultipleActiveDecoders(t *testing.T) { + server := jsonServer(t, http.StatusOK, `{}`) + defer server.Close() + + decode := func(io.Reader) (map[string]string, string, error) { return nil, "", nil } + result := VerifyToken(context.Background(), server.Client(), testToken, TokenSpec{ + Name: "invalid-contract", + Request: Request{URL: server.URL}, + Decode: decode, + DecodeResponse: func(http.Header, io.Reader) (map[string]string, string, error) { return nil, "", nil }, + }) + assert.Equal(t, finding.StatusVerifyError, result.Status) +} + func TestVerifyToken_Decode_DowngradesToInactive(t *testing.T) { server := jsonServer(t, http.StatusOK, `{"ok":false}`) defer server.Close() @@ -144,6 +241,75 @@ func TestVerifyToken_Inactive_CustomStatus403(t *testing.T) { assert.Equal(t, finding.StatusVerifiedInactive, res.Status) } +func TestVerifyToken_InactiveRequiresDefinitiveBodyWhenConfigured(t *testing.T) { + tests := []struct { + name string + contentType string + body string + decodeErr error + want finding.VerificationStatus + }{ + {name: "definitive json", contentType: "application/json", body: `{"message":"revoked"}`, want: finding.StatusVerifiedInactive}, + {name: "ambiguous json", contentType: "application/json", body: `{"error":"challenge"}`, decodeErr: errors.New("not definitive"), want: finding.StatusVerifyError}, + {name: "wrong content type", contentType: "text/html", body: `{"message":"revoked"}`, want: finding.StatusVerifyError}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", tc.contentType) + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(tc.body)) + })) + defer server.Close() + + res := VerifyToken(context.Background(), server.Client(), testToken, TokenSpec{ + Name: "x", + Request: Request{URL: server.URL}, + InactiveMessage: "secret revoked", + RequireJSONContentType: true, + DecodeInactive: func(io.Reader) error { + return tc.decodeErr + }, + }) + assert.Equal(t, tc.want, res.Status) + }) + } +} + +func TestVerifyToken_InactiveRequiresJSONWithoutDecoder(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`proxy login`)) + })) + defer server.Close() + + result := VerifyToken(context.Background(), server.Client(), testToken, TokenSpec{ + Name: "x", + Request: Request{URL: server.URL}, + InactiveMessage: "secret revoked", + RequireJSONContentType: true, + }) + assert.Equal(t, finding.StatusVerifyError, result.Status) +} + +func TestVerifyToken_InactiveBodyIsBounded(t *testing.T) { + server := jsonServer(t, http.StatusUnauthorized, strings.Repeat("x", int(MaxBodyBytes)+1)) + defer server.Close() + + result := VerifyToken(context.Background(), server.Client(), testToken, TokenSpec{ + Name: "x", + Request: Request{URL: server.URL}, + RequireJSONContentType: true, + DecodeInactive: func(io.Reader) error { + t.Fatal("oversized inactive body must not reach the decoder") + return nil + }, + }) + assert.Equal(t, finding.StatusVerifyError, result.Status) + assert.Contains(t, result.Message, "exceeds") +} + func TestVerifyToken_CustomActiveStatus405(t *testing.T) { server := jsonServer(t, http.StatusMethodNotAllowed, `{}`) defer server.Close() @@ -195,8 +361,10 @@ func TestVerifyToken_RateLimited_429IsDistinguished(t *testing.T) { defer server.Close() res := VerifyToken(context.Background(), server.Client(), testToken, TokenSpec{ - Name: "x", - Request: Request{URL: server.URL}, + Name: "x", + // POST is deliberately not retried: the shared helper only retries safe + // GET/HEAD probes. + Request: Request{Method: http.MethodPost, URL: server.URL}, }) assert.Equal(t, finding.StatusVerifyError, res.Status) assert.Contains(t, res.Message, "rate limited by provider") @@ -206,6 +374,282 @@ func TestVerifyToken_RateLimited_429IsDistinguished(t *testing.T) { assert.NotContains(t, res.Message, "unexpected status code") } +func TestVerifyToken_RateLimitedGETRetriesOnceAndSucceeds(t *testing.T) { + var logs bytes.Buffer + previous := slog.Default() + slog.SetDefault(slog.New(slog.NewTextHandler(&logs, &slog.HandlerOptions{Level: slog.LevelDebug}))) + t.Cleanup(func() { slog.SetDefault(previous) }) + + var calls atomic.Int64 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + if calls.Add(1) == 1 { + w.Header().Set("Retry-After", "0") + w.WriteHeader(http.StatusTooManyRequests) + return + } + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + result := VerifyToken(context.Background(), server.Client(), testToken, TokenSpec{ + Name: "x", + Request: Request{URL: server.URL}, + ActiveMessage: "active after retry", + }) + assert.Equal(t, finding.StatusVerifiedActive, result.Status) + assert.Equal(t, int64(2), calls.Load()) + assert.Contains(t, logs.String(), "max_attempts=2") + assert.Contains(t, logs.String(), "max_total_wait=2s") + assert.NotContains(t, logs.String(), testToken) +} + +func TestVerifyToken_RateLimitedGETHasStrictAttemptBound(t *testing.T) { + var calls atomic.Int64 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + calls.Add(1) + w.Header().Set("Retry-After", "0") + w.WriteHeader(http.StatusTooManyRequests) + })) + defer server.Close() + + result := VerifyToken(context.Background(), server.Client(), testToken, TokenSpec{ + Name: "x", + Request: Request{URL: server.URL}, + }) + assert.Equal(t, finding.StatusVerifyError, result.Status) + assert.Contains(t, result.Message, "rate limited") + assert.Equal(t, int64(max429Attempts), calls.Load()) +} + +func TestVerifyToken_RateLimitedDoesNotRetryUnsafeOrUnscheduledRequests(t *testing.T) { + for _, tc := range []struct { + name string + method string + retryAfter string + }{ + {name: "unsafe post", method: http.MethodPost, retryAfter: "0"}, + {name: "missing Retry-After", method: http.MethodGet}, + {name: "invalid Retry-After", method: http.MethodGet, retryAfter: "not-a-delay"}, + {name: "wait exceeds bound", method: http.MethodGet, retryAfter: "3"}, + } { + t.Run(tc.name, func(t *testing.T) { + var calls atomic.Int64 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + calls.Add(1) + if tc.retryAfter != "" { + w.Header().Set("Retry-After", tc.retryAfter) + } + w.WriteHeader(http.StatusTooManyRequests) + })) + defer server.Close() + + result := VerifyToken(context.Background(), server.Client(), testToken, TokenSpec{ + Name: "x", + Request: Request{Method: tc.method, URL: server.URL}, + }) + assert.Equal(t, finding.StatusVerifyError, result.Status) + assert.Equal(t, int64(1), calls.Load()) + }) + } +} + +func TestVerifyToken_RetryDeadlineAndAdmissionGate(t *testing.T) { + t.Run("request gate admits every actual send", func(t *testing.T) { + var calls atomic.Int64 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + if calls.Add(1) == 1 { + w.Header().Set("Retry-After", "0") + w.WriteHeader(http.StatusTooManyRequests) + return + } + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + gateCalls := 0 + ctx := WithRequestGate(context.Background(), func() *finding.VerificationResult { + gateCalls++ + return nil + }) + result := VerifyToken(ctx, server.Client(), testToken, TokenSpec{Name: "x", Request: Request{URL: server.URL}}) + assert.Equal(t, finding.StatusVerifiedActive, result.Status) + assert.Equal(t, int64(2), calls.Load()) + assert.Equal(t, 2, gateCalls) + }) + + t.Run("request gate rejection prevents initial transport", func(t *testing.T) { + var calls atomic.Int64 + client := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) { + calls.Add(1) + return nil, errors.New("transport must not run") + })} + ctx := WithRequestGate(context.Background(), func() *finding.VerificationResult { + return &finding.VerificationResult{Status: finding.StatusVerifyError, Message: "admission rejected"} + }) + + result := VerifyToken(ctx, client, testToken, TokenSpec{ + Name: "x", + Request: Request{URL: "https://provider.example.test/probe"}, + }) + assert.Equal(t, finding.StatusVerifyError, result.Status) + assert.Equal(t, "admission rejected", result.Message) + assert.Zero(t, calls.Load()) + }) + + t.Run("empty token consumes no request admission", func(t *testing.T) { + gateCalls := 0 + ctx := WithRequestGate(context.Background(), func() *finding.VerificationResult { + gateCalls++ + return nil + }) + result := VerifyToken(ctx, nil, "", TokenSpec{Name: "x", Request: Request{URL: "https://provider.example.test"}}) + assert.Equal(t, finding.StatusUnverified, result.Status) + assert.Zero(t, gateCalls) + }) + + t.Run("deadline too short prevents wait and request", func(t *testing.T) { + var calls atomic.Int64 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + calls.Add(1) + w.Header().Set("Retry-After", "1") + w.WriteHeader(http.StatusTooManyRequests) + })) + defer server.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond) + defer cancel() + started := time.Now() + result := VerifyToken(ctx, server.Client(), testToken, TokenSpec{Name: "x", Request: Request{URL: server.URL}}) + assert.Equal(t, finding.StatusVerifyError, result.Status) + assert.Equal(t, int64(1), calls.Load()) + assert.Less(t, time.Since(started), 200*time.Millisecond) + }) + + t.Run("gate rejection prevents retry request", func(t *testing.T) { + var calls atomic.Int64 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + calls.Add(1) + w.Header().Set("Retry-After", "0") + w.WriteHeader(http.StatusTooManyRequests) + })) + defer server.Close() + + gateCalls := 0 + ctx := WithRetryGate(context.Background(), func() *finding.VerificationResult { + gateCalls++ + return &finding.VerificationResult{Status: finding.StatusVerifyError, Message: "admission rejected"} + }) + result := VerifyToken(ctx, server.Client(), testToken, TokenSpec{Name: "x", Request: Request{URL: server.URL}}) + assert.Equal(t, finding.StatusVerifyError, result.Status) + assert.Equal(t, "admission rejected", result.Message) + assert.Equal(t, 1, gateCalls) + assert.Equal(t, int64(1), calls.Load()) + }) +} + +func TestAddRetryJitter_IsBoundedAndNeverShortensRetryAfter(t *testing.T) { + base := time.Second + assert.Equal(t, base, addRetryJitter(base, max429RetryWait, 0)) + assert.Equal(t, 1100*time.Millisecond, addRetryJitter(base, max429RetryWait, 1)) + assert.Equal(t, max429RetryWait, addRetryJitter(1950*time.Millisecond, max429RetryWait, 1)) + for range 100 { + unit := retryJitterUnit() + assert.GreaterOrEqual(t, unit, float64(0)) + assert.LessOrEqual(t, unit, float64(1)) + } +} + +func TestVerifyToken_RetryAndDecodePanicAlwaysCloseResponseBodies(t *testing.T) { + t.Run("redirect drains and closes response", func(t *testing.T) { + var closed atomic.Bool + client := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusTemporaryRedirect, + Header: make(http.Header), + Body: &closeTrackingBody{Reader: strings.NewReader("redirect body"), closed: &closed}, + }, nil + })} + + result := VerifyToken(context.Background(), client, testToken, TokenSpec{ + Name: "x", + Request: Request{URL: "https://provider.example.test/probe"}, + }) + assert.Equal(t, finding.StatusVerifyError, result.Status) + assert.Contains(t, result.Message, "redirect") + assert.True(t, closed.Load()) + }) + + t.Run("retry closes the 429 response before the next request", func(t *testing.T) { + var ( + calls atomic.Int64 + firstClosed atomic.Bool + lastClosed atomic.Bool + ) + client := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) { + if calls.Add(1) == 1 { + return &http.Response{ + StatusCode: http.StatusTooManyRequests, + Header: http.Header{"Retry-After": []string{"0"}}, + Body: &closeTrackingBody{Reader: strings.NewReader("rate limited"), closed: &firstClosed}, + }, nil + } + return &http.Response{ + StatusCode: http.StatusOK, + Header: make(http.Header), + Body: &closeTrackingBody{Reader: strings.NewReader("ok"), closed: &lastClosed}, + }, nil + })} + + result := VerifyToken(context.Background(), client, testToken, TokenSpec{ + Name: "x", + Request: Request{URL: "https://provider.example.test/probe"}, + ActiveMessage: "active", + }) + assert.Equal(t, finding.StatusVerifiedActive, result.Status) + assert.True(t, firstClosed.Load()) + assert.True(t, lastClosed.Load()) + }) + + t.Run("provider decode panic still closes final response", func(t *testing.T) { + var closed atomic.Bool + client := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + Header: make(http.Header), + Body: &closeTrackingBody{Reader: strings.NewReader(`{}`), closed: &closed}, + }, nil + })} + assert.Panics(t, func() { + VerifyToken(context.Background(), client, testToken, TokenSpec{ + Name: "x", + Request: Request{URL: "https://provider.example.test/probe"}, + Decode: func(io.Reader) (map[string]string, string, error) { + panic("synthetic decoder panic") + }, + }) + }) + assert.True(t, closed.Load()) + }) +} + +func TestRetryAfterDelay_ParsesBoundedly(t *testing.T) { + now := time.Date(2026, 8, 11, 12, 0, 0, 0, time.UTC) + delay, ok := retryAfterDelay("1", now) + require.True(t, ok) + assert.Equal(t, time.Second, delay) + + delay, ok = retryAfterDelay(now.Add(time.Second).Format(http.TimeFormat), now) + require.True(t, ok) + assert.Equal(t, time.Second, delay) + + delay, ok = retryAfterDelay("18446744073709551615", now) + require.True(t, ok) + assert.Greater(t, delay, max429RetryWait) + + _, ok = retryAfterDelay("invalid", now) + assert.False(t, ok) +} + func TestRateLimited(t *testing.T) { withHeader := RateLimited(context.Background(), "x", "12") assert.Equal(t, finding.StatusVerifyError, withHeader.Status) @@ -217,6 +661,26 @@ func TestRateLimited(t *testing.T) { assert.NotContains(t, noHeader.Message, "Retry-After") } +func TestRateLimited_DoesNotReflectArbitraryHeaderText(t *testing.T) { + var logs bytes.Buffer + previous := slog.Default() + slog.SetDefault(slog.New(slog.NewTextHandler(&logs, &slog.HandlerOptions{Level: slog.LevelDebug}))) + t.Cleanup(func() { slog.SetDefault(previous) }) + + result := RateLimited(context.Background(), "x", testToken) + assert.Equal(t, finding.StatusVerifyError, result.Status) + assert.NotContains(t, result.Message, testToken) + assert.NotContains(t, logs.String(), testToken) + assert.NotContains(t, result.Message, "Retry-After") + + httpDate := "Wed, 21 Oct 2015 07:28:00 GMT" + result = RateLimited(context.Background(), "x", httpDate) + assert.Contains(t, result.Message, "Retry-After: "+httpDate) + + result = RateLimited(context.Background(), "x", " 00030 ") + assert.Contains(t, result.Message, "Retry-After: 30") +} + func TestVerifyToken_Redirect_IsVerifyError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Location", "https://example.com/login") @@ -249,6 +713,48 @@ func TestVerifyToken_TransportError_RedactsSecretInURL(t *testing.T) { assert.Contains(t, res.Message, "[REDACTED]") } +func TestVerifyToken_TransportError_AlwaysRedactsTokenArgument(t *testing.T) { + var logs bytes.Buffer + previous := slog.Default() + slog.SetDefault(slog.New(slog.NewTextHandler(&logs, &slog.HandlerOptions{Level: slog.LevelDebug}))) + t.Cleanup(func() { slog.SetDefault(previous) }) + + client := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) { + return nil, fmt.Errorf("proxy echoed header credential %s", testToken) + })} + + res := VerifyToken(context.Background(), client, testToken, TokenSpec{ + Name: "x", + Request: Request{URL: "https://api.example.invalid"}, + }) + require.Equal(t, finding.StatusVerifyError, res.Status) + assert.NotContains(t, res.Message, testToken) + assert.Contains(t, res.Message, "[REDACTED]") + assert.NotContains(t, logs.String(), testToken) +} + +func TestVerifyToken_TransportError_RedactsDerivedBasicAuthorization(t *testing.T) { + const user = "synthetic-user" + const password = "synthetic-password-1234" + encoded := base64.StdEncoding.EncodeToString([]byte(user + ":" + password)) + client := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) { + return nil, fmt.Errorf("proxy echoed Authorization: Basic %s", encoded) + })} + + res := VerifyToken(context.Background(), client, password, TokenSpec{ + Name: "x", + Request: Request{ + URL: "https://api.example.invalid", + BasicAuthUser: user, + BasicAuthPass: password, + }, + }) + require.Equal(t, finding.StatusVerifyError, res.Status) + assert.NotContains(t, res.Message, encoded) + assert.NotContains(t, res.Message, password) + assert.Contains(t, res.Message, "[REDACTED]") +} + func TestVerifyToken_BasicAuthAndHeaders(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user, pass, ok := r.BasicAuth() diff --git a/internal/verifier/internal/vtest/vtest.go b/internal/verifier/internal/vtest/vtest.go index 4d7186a..0eb7c3b 100644 --- a/internal/verifier/internal/vtest/vtest.go +++ b/internal/verifier/internal/vtest/vtest.go @@ -14,9 +14,16 @@ package vtest import ( + "bytes" "context" + "fmt" + "log/slog" "net/http" "net/http/httptest" + "net/url" + "strings" + "sync" + "sync/atomic" "testing" "github.com/stretchr/testify/assert" @@ -44,6 +51,15 @@ type Case struct { // plausibly formatted secret so the verifier reaches the HTTP call. Raw detector.RawFinding + // RawForURL optionally builds the finding after the hermetic endpoint is + // known. It is used by credentials whose request destination is the finding + // itself, such as provider webhook URLs. + RawForURL func(string) detector.RawFinding + + // AdditionalSensitive lists credential-bearing representations that the + // verifier derives outside Raw/RawV2 and must redact from results and logs. + AdditionalSensitive [][]byte + // MalformedStatus is the status the verifier returns for a 200 response // whose body is not valid JSON. Defaults to StatusVerifyError when zero // (the project standard). Set explicitly for verifiers that only inspect @@ -74,10 +90,35 @@ func Run(t *testing.T, c Case) { server.Close() // Force a connection-refused transport error. v := c.New(url, client) - result := v.Verify(context.Background(), c.Raw) + testCase := c.withURL(url) + result := v.Verify(context.Background(), testCase.Raw) assert.Equal(t, finding.StatusVerifyError, result.Status, "a transport error must be a verify error") + assertSecretAbsent(t, result, "", testCase) + }) + + t.Run(c.Name+"/transport_error_cannot_echo_secret", func(t *testing.T) { + var calls atomic.Int32 + const endpoint = "https://api.example.invalid" + testCase := c.withURL(endpoint) + client := &http.Client{Transport: roundTripFunc(func(request *http.Request) (*http.Response, error) { + calls.Add(1) + echoed := []string{request.URL.String(), string(testCase.Raw.Raw), string(testCase.Raw.RawV2)} + for _, values := range request.Header { + echoed = append(echoed, values...) + } + return nil, fmt.Errorf("synthetic transport failure echoed request %q", echoed) + })} + v := c.New(endpoint, client) + result, logs := verifyWithCapturedLogs(t, func() finding.VerificationResult { + return v.Verify(context.Background(), testCase.Raw) + }) + + require.Equal(t, int32(1), calls.Load(), "injected transport must be exercised exactly once") + assert.Equal(t, finding.StatusVerifyError, result.Status, + "a transport error must be a verify error") + assertSecretAbsent(t, result, logs, testCase) }) t.Run(c.Name+"/cancelled_context_is_not_inactive", func(t *testing.T) { @@ -90,13 +131,15 @@ func Run(t *testing.T, c Case) { ctx, cancel := context.WithCancel(context.Background()) cancel() // Cancel immediately. + testCase := c.withURL(server.URL) v := c.New(server.URL, server.Client()) - result := v.Verify(ctx, c.Raw) + result := v.Verify(ctx, testCase.Raw) require.NotEqual(t, finding.StatusVerifiedInactive, result.Status, "a cancelled context must NOT be reported as verified-inactive") assert.Equal(t, finding.StatusVerifyError, result.Status, "a cancelled context must be a verify error") + assertSecretAbsent(t, result, "", testCase) }) if c.SkipMalformed { @@ -104,7 +147,9 @@ func Run(t *testing.T, c Case) { } t.Run(c.Name+"/malformed_body_has_defined_status", func(t *testing.T) { + var calls atomic.Int32 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + calls.Add(1) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{not valid json`)) @@ -119,12 +164,65 @@ func Run(t *testing.T, c Case) { want = finding.StatusVerifyError } + testCase := c.withURL(server.URL) v := c.New(server.URL, server.Client()) - result := v.Verify(context.Background(), c.Raw) + result := v.Verify(context.Background(), testCase.Raw) + require.Equal(t, int32(1), calls.Load(), "malformed-response transport must be exercised exactly once") assert.Equal(t, want, result.Status, "a 200 with a malformed body must have a defined status") assert.NotEqual(t, finding.StatusVerifiedInactive, result.Status, "a malformed 200 body must never be reported as verified-inactive") + assertSecretAbsent(t, result, "", testCase) }) } + +func (c Case) withURL(endpoint string) Case { + if c.RawForURL != nil { + c.Raw = c.RawForURL(endpoint) + } + return c +} + +type roundTripFunc func(*http.Request) (*http.Response, error) + +func (f roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) { + return f(request) +} + +func assertSecretAbsent(t *testing.T, result finding.VerificationResult, logs string, c Case) { + t.Helper() + secrets := append([][]byte{c.Raw.Raw, c.Raw.RawV2}, c.AdditionalSensitive...) + for _, secret := range secrets { + if len(secret) < 8 { + continue + } + for _, form := range sensitiveForms(string(secret)) { + assert.NotContains(t, result.Message, form, "verification message leaked credential") + assert.NotContains(t, logs, form, "verification log leaked credential") + } + for key, value := range result.ExtraData { + for _, form := range sensitiveForms(string(secret)) { + assert.False(t, strings.Contains(value, form), + "verification ExtraData[%q] leaked credential", key) + } + } + } +} + +func sensitiveForms(secret string) []string { + return []string{secret, url.PathEscape(secret), url.QueryEscape(secret)} +} + +var slogCaptureMu sync.Mutex + +func verifyWithCapturedLogs(t *testing.T, verify func() finding.VerificationResult) (finding.VerificationResult, string) { + t.Helper() + slogCaptureMu.Lock() + defer slogCaptureMu.Unlock() + var logs bytes.Buffer + previous := slog.Default() + slog.SetDefault(slog.New(slog.NewTextHandler(&logs, &slog.HandlerOptions{Level: slog.LevelDebug}))) + defer slog.SetDefault(previous) + return verify(), logs.String() +} diff --git a/internal/verifier/launchdarkly/launchdarkly_shared_test.go b/internal/verifier/launchdarkly/launchdarkly_shared_test.go new file mode 100644 index 0000000..bf3276a --- /dev/null +++ b/internal/verifier/launchdarkly/launchdarkly_shared_test.go @@ -0,0 +1,16 @@ +package launchdarkly + +import ( + "net/http" + "testing" + + "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/internal/verifier" + "github.com/HodeTech/leakwatch/internal/verifier/internal/vtest" +) + +func TestVerify_SharedSafetySuite(t *testing.T) { + vtest.Run(t, vtest.Case{Name: "launchdarkly", New: func(url string, client *http.Client) verifier.Verifier { + return &Verifier{apiURL: url, httpClient: client} + }, Raw: detector.RawFinding{DetectorID: detectorID, Raw: []byte("sdk-abcdef01-2345-6789-abcd-0123456789ab")}, SkipMalformed: true}) +} diff --git a/internal/verifier/mailgun/mailgun_verifier.go b/internal/verifier/mailgun/mailgun_verifier.go index ab0c4d7..f4e11be 100644 --- a/internal/verifier/mailgun/mailgun_verifier.go +++ b/internal/verifier/mailgun/mailgun_verifier.go @@ -57,17 +57,60 @@ func (v *Verifier) Type() string { // first probe is never treated as "inactive", so it is returned as-is // without a second call. func (v *Verifier) Verify(ctx context.Context, raw detector.RawFinding) finding.VerificationResult { + return v.verify(ctx, raw, nil) +} + +// VerificationRequestBudget allows one initial request and one bounded HTTP +// 429 replay in each of Mailgun's two fixed regions. +func (v *Verifier) VerificationRequestBudget() int { return 4 } + +// VerifyWithRequestGate admits every actual regional send through the engine's +// global and per-provider limiters. HTTP 429 replays receive the same gate via +// the shared httpx context. +func (v *Verifier) VerifyWithRequestGate( + ctx context.Context, + raw detector.RawFinding, + gate verifier.RequestGate, +) finding.VerificationResult { + return v.verify(ctx, raw, gate) +} + +func (v *Verifier) verify( + ctx context.Context, + raw detector.RawFinding, + gate verifier.RequestGate, +) finding.VerificationResult { token := string(raw.Raw) + if token == "" { + return finding.VerificationResult{ + Status: finding.StatusUnverified, + Message: "empty token", + } + } usURL := httpx.BaseURL(v.apiURL, defaultAPIURL) - result := verifyDomains(ctx, v.httpClient, token, usURL) + result := v.verifyRegion(ctx, token, usURL, gate) if result.Status != finding.StatusVerifiedInactive { return result } slog.DebugContext(ctx, "mailgun verifier: US host reports inactive, retrying against EU host") euURL := httpx.BaseURL(v.euAPIURL, defaultEUAPIURL) - return verifyDomains(ctx, v.httpClient, token, euURL) + return v.verifyRegion(ctx, token, euURL, gate) +} + +func (v *Verifier) verifyRegion( + ctx context.Context, + token, apiURL string, + gate verifier.RequestGate, +) finding.VerificationResult { + if gate != nil { + if rejection := gate(); rejection != nil { + return *rejection + } + ctx = httpx.WithRetryGate(ctx, httpx.RetryGate(gate)) + } + return verifyDomains(ctx, v.httpClient, token, apiURL) } // verifyDomains performs the Mailgun GET /v3/domains check against apiURL. diff --git a/internal/verifier/mailgun/mailgun_verifier_test.go b/internal/verifier/mailgun/mailgun_verifier_test.go index 4bfe9ed..1168dd8 100644 --- a/internal/verifier/mailgun/mailgun_verifier_test.go +++ b/internal/verifier/mailgun/mailgun_verifier_test.go @@ -4,15 +4,44 @@ import ( "context" "net/http" "net/http/httptest" + "sync" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/internal/verifier" "github.com/HodeTech/leakwatch/pkg/finding" ) +type statusSequence struct { + mu sync.Mutex + statuses []int + calls int +} + +func (s *statusSequence) handler(w http.ResponseWriter, _ *http.Request) { + s.mu.Lock() + index := s.calls + s.calls++ + status := s.statuses[len(s.statuses)-1] + if index < len(s.statuses) { + status = s.statuses[index] + } + s.mu.Unlock() + if status == http.StatusTooManyRequests { + w.Header().Set("Retry-After", "0") + } + w.WriteHeader(status) +} + +func (s *statusSequence) callCount() int { + s.mu.Lock() + defer s.mu.Unlock() + return s.calls +} + func TestVerify_ValidKey_ReturnsActive(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "/v3/domains", r.URL.Path) @@ -154,3 +183,87 @@ func TestVerify_EmptyToken_ReturnsUnverified(t *testing.T) { assert.Equal(t, finding.StatusUnverified, result.Status) assert.Equal(t, "empty token", result.Message) } + +func TestVerifyWithRequestGate_AdmitsEveryFallbackAnd429Send(t *testing.T) { + tests := []struct { + name string + usStatuses []int + euStatuses []int + wantCalls int + }{ + {name: "US inactive then EU active", usStatuses: []int{http.StatusUnauthorized}, euStatuses: []int{http.StatusOK}, wantCalls: 2}, + {name: "US 429 retry inactive then EU active", usStatuses: []int{http.StatusTooManyRequests, http.StatusUnauthorized}, euStatuses: []int{http.StatusOK}, wantCalls: 3}, + {name: "US inactive then EU 429 retry active", usStatuses: []int{http.StatusUnauthorized}, euStatuses: []int{http.StatusTooManyRequests, http.StatusOK}, wantCalls: 3}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + us := &statusSequence{statuses: tc.usStatuses} + eu := &statusSequence{statuses: tc.euStatuses} + usServer := httptest.NewServer(http.HandlerFunc(us.handler)) + defer usServer.Close() + euServer := httptest.NewServer(http.HandlerFunc(eu.handler)) + defer euServer.Close() + + v := &Verifier{apiURL: usServer.URL, euAPIURL: euServer.URL, httpClient: usServer.Client()} + gateCalls := 0 + result := v.VerifyWithRequestGate(context.Background(), detector.RawFinding{ + DetectorID: detectorID, + Raw: []byte("key-synthetic-region-sequence-123456"), + Redacted: "key-****3456", + }, func() *finding.VerificationResult { + gateCalls++ + return nil + }) + + assert.Equal(t, finding.StatusVerifiedActive, result.Status) + assert.Equal(t, tc.wantCalls, us.callCount()+eu.callCount()) + assert.Equal(t, tc.wantCalls, gateCalls, "each actual HTTP send needs one admission") + }) + } +} + +func TestVerifyWithRequestGate_RejectionPreventsEURequest(t *testing.T) { + us := &statusSequence{statuses: []int{http.StatusUnauthorized}} + eu := &statusSequence{statuses: []int{http.StatusOK}} + usServer := httptest.NewServer(http.HandlerFunc(us.handler)) + defer usServer.Close() + euServer := httptest.NewServer(http.HandlerFunc(eu.handler)) + defer euServer.Close() + + v := &Verifier{apiURL: usServer.URL, euAPIURL: euServer.URL, httpClient: usServer.Client()} + gateCalls := 0 + result := v.VerifyWithRequestGate(context.Background(), detector.RawFinding{ + DetectorID: detectorID, + Raw: []byte("key-synthetic-gate-reject-123456789"), + Redacted: "key-****6789", + }, func() *finding.VerificationResult { + gateCalls++ + if gateCalls == 2 { + return &finding.VerificationResult{Status: finding.StatusVerifyError, Message: "admission rejected"} + } + return nil + }) + + assert.Equal(t, finding.StatusVerifyError, result.Status) + assert.Equal(t, "admission rejected", result.Message) + assert.Equal(t, 1, us.callCount()) + assert.Zero(t, eu.callCount()) +} + +func TestVerificationRequestBudget_CoversTwoRegionsAndRetries(t *testing.T) { + v := &Verifier{} + assert.Equal(t, 4, v.VerificationRequestBudget()) + var _ verifier.RequestGatedVerifier = v +} + +func TestVerifyWithRequestGate_EmptyTokenConsumesNoAdmission(t *testing.T) { + v := &Verifier{} + gateCalls := 0 + result := v.VerifyWithRequestGate(context.Background(), detector.RawFinding{}, func() *finding.VerificationResult { + gateCalls++ + return nil + }) + + assert.Equal(t, finding.StatusUnverified, result.Status) + assert.Zero(t, gateCalls) +} diff --git a/internal/verifier/newrelic/newrelic_shared_test.go b/internal/verifier/newrelic/newrelic_shared_test.go new file mode 100644 index 0000000..bba1fbf --- /dev/null +++ b/internal/verifier/newrelic/newrelic_shared_test.go @@ -0,0 +1,24 @@ +package newrelic + +import ( + "net/http" + "testing" + + "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/internal/verifier" + "github.com/HodeTech/leakwatch/internal/verifier/internal/vtest" +) + +func TestVerify_SharedSafetySuite(t *testing.T) { + vtest.Run(t, vtest.Case{ + Name: "newrelic", + New: func(apiURL string, client *http.Client) verifier.Verifier { + return &Verifier{endpoints: []endpoint{{region: "test", url: apiURL}}, httpClient: client} + }, + Raw: detector.RawFinding{ + DetectorID: detectorID, + Raw: []byte(testToken), + Redacted: "NRAK-****2MNO", + }, + }) +} diff --git a/internal/verifier/newrelic/newrelic_verifier.go b/internal/verifier/newrelic/newrelic_verifier.go index d130348..e3ab5f3 100644 --- a/internal/verifier/newrelic/newrelic_verifier.go +++ b/internal/verifier/newrelic/newrelic_verifier.go @@ -1,10 +1,16 @@ -// Package newrelic provides a verifier for New Relic API keys. -// It uses the New Relic API GET /v2/users.json endpoint to check token validity. +// Package newrelic provides a verifier for New Relic user API keys. +// It uses a read-only NerdGraph requestContext query against New Relic's +// officially documented US and EU endpoints. package newrelic import ( "context" + "encoding/json" + "fmt" + "io" "net/http" + "strconv" + "strings" "github.com/HodeTech/leakwatch/internal/detector" "github.com/HodeTech/leakwatch/internal/verifier" @@ -12,17 +18,30 @@ import ( "github.com/HodeTech/leakwatch/pkg/finding" ) -const detectorID = "newrelic-api-key" +const ( + detectorID = "newrelic-api-key" -// defaultAPIURL is the base URL for the New Relic API. -const defaultAPIURL = "https://api.newrelic.com" + usNerdGraphURL = "https://api.newrelic.com/graphql" + euNerdGraphURL = "https://api.eu.newrelic.com/graphql" -// Verifier checks whether a New Relic API key is active by calling the -// New Relic API. It NEVER logs or persists raw token values. + requestContextQuery = `{"query":"query LeakwatchVerifyKey { requestContext { userId } }"}` +) + +type endpoint struct { + region string + url string +} + +var officialEndpoints = []endpoint{ + {region: "US", url: usNerdGraphURL}, + {region: "EU", url: euNerdGraphURL}, +} + +// Verifier checks whether a New Relic user API key is active. It NEVER logs or +// persists raw token values. endpoints and httpClient are hermetic test seams; +// production uses the fixed officialEndpoints list and hardened shared client. type Verifier struct { - // apiURL overrides the New Relic API base URL (for testing). - apiURL string - // httpClient overrides the default HTTP client (for testing). + endpoints []endpoint httpClient *http.Client } @@ -35,20 +54,176 @@ func (v *Verifier) Type() string { return detectorID } -// Verify checks if the detected New Relic API key is valid/active. -// Raw contains the token value. +// VerificationRequestBudget returns the maximum number of HTTP probes this +// verifier can issue for one finding. The verification engine enforces this +// bound while admitting each actual request at its send point. +func (v *Verifier) VerificationRequestBudget() int { + return len(v.resolvedEndpoints()) +} + +// Verify checks the key against New Relic's fixed US/EU NerdGraph endpoints. +// A 401 from one region is not revocation evidence because the key may belong +// to the other region. The key is inactive only when every supported region +// returns 401; any successful region wins, while permission/network/provider +// errors keep the outcome inconclusive. func (v *Verifier) Verify(ctx context.Context, raw detector.RawFinding) finding.VerificationResult { + return v.verify(ctx, raw, nil) +} + +// VerifyWithRequestGate checks every actual regional request through the +// engine-owned rate limiter immediately before sending it. The fallback region +// consumes no token unless it is genuinely attempted. +func (v *Verifier) VerifyWithRequestGate( + ctx context.Context, + raw detector.RawFinding, + gate verifier.RequestGate, +) finding.VerificationResult { + return v.verify(ctx, raw, gate) +} + +func (v *Verifier) verify( + ctx context.Context, + raw detector.RawFinding, + gate verifier.RequestGate, +) finding.VerificationResult { token := string(raw.Raw) - apiURL := httpx.BaseURL(v.apiURL, defaultAPIURL) + if strings.TrimSpace(token) == "" { + return finding.VerificationResult{Status: finding.StatusUnverified, Message: "empty token"} + } + + endpoints := v.resolvedEndpoints() + unauthorized := 0 + hadInconclusive := false + for _, target := range endpoints { + if ctx.Err() != nil { + return finding.VerificationResult{ + Status: finding.StatusVerifyError, + Message: "New Relic verification cancelled before all regions were checked", + } + } + if gate != nil { + if rejection := gate(); rejection != nil { + return *rejection + } + } + result := v.verifyEndpoint(ctx, token, target) + switch result.Status { + case finding.StatusVerifiedActive: + return result + case finding.StatusVerifiedInactive: + unauthorized++ + default: + hadInconclusive = true + } + } + + if unauthorized == len(endpoints) && !hadInconclusive { + return finding.VerificationResult{ + Status: finding.StatusVerifiedInactive, + Message: "New Relic API key is invalid or revoked in all supported regions", + } + } + return finding.VerificationResult{ + Status: finding.StatusVerifyError, + Message: "New Relic verification was inconclusive across supported regions", + } +} + +func (v *Verifier) resolvedEndpoints() []endpoint { + if len(v.endpoints) > 0 { + return v.endpoints + } + return officialEndpoints +} + +func (v *Verifier) verifyEndpoint(ctx context.Context, token string, target endpoint) finding.VerificationResult { return httpx.VerifyToken(ctx, v.httpClient, token, httpx.TokenSpec{ - Name: "newrelic", + Name: "newrelic-" + strings.ToLower(target.region), Request: httpx.Request{ - URL: apiURL + "/v2/users.json", - Header: map[string]string{"Api-Key": token}, + Method: http.MethodPost, + URL: target.url, + Body: []byte(requestContextQuery), + Header: map[string]string{ + "Accept": "application/json", + "Api-Key": token, + "Content-Type": "application/json", + }, }, - InactiveStatuses: []int{http.StatusUnauthorized, http.StatusForbidden}, - ActiveMessage: "New Relic API key is active", - InactiveMessage: "New Relic API key is invalid or revoked", + ActiveMessage: "New Relic API key is active (" + target.region + " region)", + InactiveMessage: "New Relic API key was rejected by the " + target.region + " region", + Decode: decodeRequestContext, + DecodeInactive: decodeInactiveResponse, + RequireCompleteBody: true, + RequireJSONContentType: true, }) } + +func decodeInactiveResponse(body io.Reader) error { + var response struct { + Errors []struct { + Message string `json:"message"` + } `json:"errors"` + } + decoder := json.NewDecoder(body) + if err := decoder.Decode(&response); err != nil { + return err + } + if len(response.Errors) == 0 { + return fmt.Errorf("missing New Relic authentication error") + } + for _, item := range response.Errors { + if strings.ToLower(strings.TrimSpace(item.Message)) != "authentication required" { + return fmt.Errorf("unexpected New Relic authentication error") + } + } + if decoder.Decode(&struct{}{}) != io.EOF { + return fmt.Errorf("unexpected trailing New Relic response data") + } + return nil +} + +func decodeRequestContext(body io.Reader) (map[string]string, string, error) { + var response struct { + Data *struct { + RequestContext *struct { + UserID json.RawMessage `json:"userId"` + } `json:"requestContext"` + } `json:"data"` + Errors []json.RawMessage `json:"errors"` + } + decoder := json.NewDecoder(body) + if err := decoder.Decode(&response); err != nil { + return nil, "", fmt.Errorf("invalid NerdGraph response: %w", err) + } + if len(response.Errors) > 0 { + return nil, "", fmt.Errorf("NerdGraph returned GraphQL errors") + } + if response.Data == nil || response.Data.RequestContext == nil || + !isValidUserID(response.Data.RequestContext.UserID) { + return nil, "", fmt.Errorf("NerdGraph response did not identify the API key owner") + } + + var trailing json.RawMessage + if err := decoder.Decode(&trailing); err != io.EOF { + if err == nil { + return nil, "", fmt.Errorf("invalid NerdGraph response: trailing JSON value") + } + return nil, "", fmt.Errorf("invalid NerdGraph response: %w", err) + } + return nil, "", nil +} + +func isValidUserID(value json.RawMessage) bool { + var text string + if err := json.Unmarshal(value, &text); err == nil { + return strings.TrimSpace(text) != "" + } + + var number json.Number + if err := json.Unmarshal(value, &number); err != nil { + return false + } + id, err := strconv.ParseInt(number.String(), 10, 64) + return err == nil && id > 0 +} diff --git a/internal/verifier/newrelic/newrelic_verifier_test.go b/internal/verifier/newrelic/newrelic_verifier_test.go index 24eec75..3492733 100644 --- a/internal/verifier/newrelic/newrelic_verifier_test.go +++ b/internal/verifier/newrelic/newrelic_verifier_test.go @@ -2,8 +2,11 @@ package newrelic import ( "context" + "io" "net/http" "net/http/httptest" + "strings" + "sync/atomic" "testing" "github.com/stretchr/testify/assert" @@ -13,122 +16,286 @@ import ( "github.com/HodeTech/leakwatch/pkg/finding" ) -func TestVerify_ValidToken_ReturnsActive(t *testing.T) { +const testToken = "NRAK-ABC123DEF456GHI789JKL012MNO" + +func rawFinding(token string) detector.RawFinding { + return detector.RawFinding{DetectorID: detectorID, Raw: []byte(token), Redacted: "NRAK-****2MNO"} +} + +func TestVerify_NerdGraphRequestAndResponseContract(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/v2/users.json", r.URL.Path) - assert.NotEmpty(t, r.Header.Get("Api-Key")) + assert.Equal(t, "/graphql", r.URL.Path) + assert.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, testToken, r.Header.Get("Api-Key")) + assert.Equal(t, "application/json", r.Header.Get("Accept")) + assert.Equal(t, "application/json", r.Header.Get("Content-Type")) + body, err := io.ReadAll(r.Body) + require.NoError(t, err) + assert.Equal(t, requestContextQuery, string(body)) w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(`{"users":[{"id":123,"email":"user@example.com"}]}`)) + _, _ = w.Write([]byte(`{"data":{"requestContext":{"userId":12345}}}`)) })) defer server.Close() v := &Verifier{ - apiURL: server.URL, + endpoints: []endpoint{{region: "US", url: server.URL + "/graphql"}}, httpClient: server.Client(), } + result := v.Verify(context.Background(), rawFinding(testToken)) + + require.Equal(t, finding.StatusVerifiedActive, result.Status) + assert.Equal(t, "New Relic API key is active (US region)", result.Message) + assert.Empty(t, result.ExtraData, "identity/PII must not be retained") +} - raw := detector.RawFinding{ - DetectorID: detectorID, - Raw: []byte("NRAK-ABC123DEF456GHI789JKL012MNO345PQ"), - Redacted: "NRAK****45PQ", +func TestVerify_RegionFallbackDecisionMatrix(t *testing.T) { + tests := []struct { + name string + statuses []int + bodies []string + want finding.VerificationStatus + wantRequests int32 + }{ + { + name: "US unauthorized then EU active", + statuses: []int{http.StatusUnauthorized, http.StatusOK}, + bodies: []string{`{"errors":[{"message":"authentication required"}]}`, `{"data":{"requestContext":{"userId":"eu-user"}}}`}, + want: finding.StatusVerifiedActive, wantRequests: 2, + }, + { + name: "US active stops fallback", + statuses: []int{http.StatusOK, http.StatusInternalServerError}, + bodies: []string{`{"data":{"requestContext":{"userId":1}}}`, `{}`}, + want: finding.StatusVerifiedActive, wantRequests: 1, + }, + { + name: "all regions unauthorized", + statuses: []int{http.StatusUnauthorized, http.StatusUnauthorized}, + bodies: []string{`{"errors":[{"message":"authentication required"}]}`, `{"errors":[{"message":"authentication required"}]}`}, + want: finding.StatusVerifiedInactive, wantRequests: 2, + }, + { + name: "403 is never inactive", + statuses: []int{http.StatusForbidden, http.StatusUnauthorized}, + bodies: []string{`{}`, `{"errors":[{"message":"authentication required"}]}`}, + want: finding.StatusVerifyError, wantRequests: 2, + }, + { + name: "provider failure plus 401 is inconclusive", + statuses: []int{http.StatusInternalServerError, http.StatusUnauthorized}, + bodies: []string{`{}`, `{"errors":[{"message":"authentication required"}]}`}, + want: finding.StatusVerifyError, wantRequests: 2, + }, + { + name: "GraphQL error plus 401 is inconclusive", + statuses: []int{http.StatusOK, http.StatusUnauthorized}, + bodies: []string{`{"errors":[{"message":"forbidden"}]}`, `{"errors":[{"message":"authentication required"}]}`}, + want: finding.StatusVerifyError, wantRequests: 2, + }, } - result := v.Verify(context.Background(), raw) + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var requests atomic.Int32 + servers := make([]*httptest.Server, len(tc.statuses)) + endpoints := make([]endpoint, len(tc.statuses)) + for i := range tc.statuses { + status, body := tc.statuses[i], tc.bodies[i] + servers[i] = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + requests.Add(1) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _, _ = w.Write([]byte(body)) + })) + defer servers[i].Close() + endpoints[i] = endpoint{region: []string{"US", "EU"}[i], url: servers[i].URL} + } - require.Equal(t, finding.StatusVerifiedActive, result.Status) - assert.Equal(t, "New Relic API key is active", result.Message) + v := &Verifier{endpoints: endpoints, httpClient: servers[0].Client()} + result := v.Verify(context.Background(), rawFinding(testToken)) + assert.Equal(t, tc.want, result.Status) + assert.Equal(t, tc.wantRequests, requests.Load()) + }) + } } -func TestVerify_Unauthorized_ReturnsInactive(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusUnauthorized) - _, _ = w.Write([]byte(`{"error":{"title":"Invalid API key"}}`)) - })) - defer server.Close() - - v := &Verifier{ - apiURL: server.URL, - httpClient: server.Client(), +func TestVerify_InactiveResponseMustBeDefinitiveJSON(t *testing.T) { + tests := []struct{ contentType, body string }{ + {contentType: "text/html", body: `proxy login`}, + {contentType: "application/json", body: `{"errors":[{"message":"challenge"}]}`}, } + for _, tc := range tests { + servers := make([]*httptest.Server, 2) + endpoints := make([]endpoint, 2) + for i := range servers { + servers[i] = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", tc.contentType) + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(tc.body)) + })) + endpoints[i] = endpoint{region: []string{"US", "EU"}[i], url: servers[i].URL} + } + result := (&Verifier{endpoints: endpoints, httpClient: servers[0].Client()}).Verify( + context.Background(), rawFinding(testToken), + ) + for _, server := range servers { + server.Close() + } + assert.Equal(t, finding.StatusVerifyError, result.Status) + } +} - raw := detector.RawFinding{ - DetectorID: detectorID, - Raw: []byte("NRAK-invalidkey123456789012345678901234"), - Redacted: "NRAK****1234", +func TestVerifyWithRequestGate_AdmitsOnlyActualRegionalRequests(t *testing.T) { + tests := []struct { + name string + statuses []int + bodies []string + wantGates int32 + wantCalls int32 + }{ + { + name: "US active does not admit unused EU fallback", + statuses: []int{http.StatusOK, http.StatusInternalServerError}, + bodies: []string{`{"data":{"requestContext":{"userId":1}}}`, `{}`}, + wantGates: 1, + wantCalls: 1, + }, + { + name: "US rejection admits EU immediately before fallback", + statuses: []int{http.StatusUnauthorized, http.StatusOK}, + bodies: []string{`{"errors":[{"message":"authentication required"}]}`, `{"data":{"requestContext":{"userId":2}}}`}, + wantGates: 2, + wantCalls: 2, + }, } - result := v.Verify(context.Background(), raw) + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var gates atomic.Int32 + var calls atomic.Int32 + servers := make([]*httptest.Server, len(tc.statuses)) + endpoints := make([]endpoint, len(tc.statuses)) + for i := range tc.statuses { + status, body := tc.statuses[i], tc.bodies[i] + servers[i] = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + calls.Add(1) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _, _ = w.Write([]byte(body)) + })) + defer servers[i].Close() + endpoints[i] = endpoint{region: []string{"US", "EU"}[i], url: servers[i].URL} + } - assert.Equal(t, finding.StatusVerifiedInactive, result.Status) - assert.Equal(t, "New Relic API key is invalid or revoked", result.Message) + v := &Verifier{endpoints: endpoints, httpClient: servers[0].Client()} + result := v.VerifyWithRequestGate(context.Background(), rawFinding(testToken), func() *finding.VerificationResult { + gates.Add(1) + return nil + }) + assert.Equal(t, finding.StatusVerifiedActive, result.Status) + assert.Equal(t, tc.wantGates, gates.Load()) + assert.Equal(t, tc.wantCalls, calls.Load()) + }) + } } -func TestVerify_Forbidden_ReturnsInactive(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusForbidden) - _, _ = w.Write([]byte(`{"error":{"title":"Forbidden"}}`)) - })) - defer server.Close() - +func TestVerifyWithRequestGate_RejectionPreventsFallbackRequest(t *testing.T) { + var requests atomic.Int32 + servers := make([]*httptest.Server, 2) + for i, status := range []int{http.StatusUnauthorized, http.StatusOK} { + status := status + servers[i] = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + requests.Add(1) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _, _ = w.Write([]byte(`{"data":{"requestContext":{"userId":2}}}`)) + })) + defer servers[i].Close() + } v := &Verifier{ - apiURL: server.URL, - httpClient: server.Client(), + endpoints: []endpoint{{region: "US", url: servers[0].URL}, {region: "EU", url: servers[1].URL}}, + httpClient: servers[0].Client(), } + var gates atomic.Int32 + result := v.VerifyWithRequestGate(context.Background(), rawFinding(testToken), func() *finding.VerificationResult { + if gates.Add(1) == 2 { + return &finding.VerificationResult{Status: finding.StatusVerifyError, Message: "rate limiter cancelled"} + } + return nil + }) + + assert.Equal(t, finding.StatusVerifyError, result.Status) + assert.Equal(t, int32(2), gates.Load()) + assert.Equal(t, int32(1), requests.Load(), "rejected admission must happen before the EU request is sent") +} - raw := detector.RawFinding{ - DetectorID: detectorID, - Raw: []byte("NRAK-forbiddenkey12345678901234567890123"), - Redacted: "NRAK****0123", +func TestVerify_InvalidSuccessBodiesAreInconclusive(t *testing.T) { + tests := []struct { + name string + contentType string + body string + }{ + {name: "HTML", contentType: "text/html", body: `login`}, + {name: "JSON with HTML media type", contentType: "text/html", body: `{"data":{"requestContext":{"userId":1}}}`}, + {name: "malformed", contentType: "application/json", body: `{not-json`}, + {name: "missing data", contentType: "application/json", body: `{}`}, + {name: "null data", contentType: "application/json", body: `{"data":null}`}, + {name: "null user", contentType: "application/json", body: `{"data":{"requestContext":{"userId":null}}}`}, + {name: "empty user", contentType: "application/json", body: `{"data":{"requestContext":{"userId":""}}}`}, + {name: "object user", contentType: "application/json", body: `{"data":{"requestContext":{"userId":{}}}}`}, + {name: "boolean user", contentType: "application/json", body: `{"data":{"requestContext":{"userId":true}}}`}, + {name: "zero user", contentType: "application/json", body: `{"data":{"requestContext":{"userId":0}}}`}, + {name: "fractional user", contentType: "application/json", body: `{"data":{"requestContext":{"userId":1.5}}}`}, + {name: "trailing JSON", contentType: "application/json", body: `{"data":{"requestContext":{"userId":1}}} {}`}, + {name: "oversized", contentType: "application/json", body: `{"data":{"requestContext":{"userId":1}}}` + strings.Repeat(" ", (1<<20)+1)}, } - result := v.Verify(context.Background(), raw) + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + if tc.contentType != "" { + w.Header().Set("Content-Type", tc.contentType) + } + _, _ = w.Write([]byte(tc.body)) + })) + defer server.Close() - assert.Equal(t, finding.StatusVerifiedInactive, result.Status) - assert.Equal(t, "New Relic API key is invalid or revoked", result.Message) + v := &Verifier{endpoints: []endpoint{{region: "US", url: server.URL}}, httpClient: server.Client()} + result := v.Verify(context.Background(), rawFinding(testToken)) + assert.Equal(t, finding.StatusVerifyError, result.Status) + assert.NotEqual(t, finding.StatusVerifiedActive, result.Status) + assert.NotEqual(t, finding.StatusVerifiedInactive, result.Status) + }) + } } -func TestVerify_ServerError_ReturnsError(t *testing.T) { +func TestVerify_EmptyOrCancelledIsSafe(t *testing.T) { + var requests atomic.Int32 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - _, _ = w.Write([]byte(`{"error":{"title":"Internal server error"}}`)) + requests.Add(1) + w.WriteHeader(http.StatusUnauthorized) })) defer server.Close() + v := &Verifier{endpoints: []endpoint{{region: "US", url: server.URL}}, httpClient: server.Client()} - v := &Verifier{ - apiURL: server.URL, - httpClient: server.Client(), - } - - raw := detector.RawFinding{ - DetectorID: detectorID, - Raw: []byte("NRAK-somekey12345678901234567890123456789"), - Redacted: "NRAK****6789", + for _, token := range []string{"", " \t\r\n"} { + result := v.Verify(context.Background(), rawFinding(token)) + assert.Equal(t, finding.StatusUnverified, result.Status) } - result := v.Verify(context.Background(), raw) - + ctx, cancel := context.WithCancel(context.Background()) + cancel() + result := v.Verify(ctx, rawFinding(testToken)) assert.Equal(t, finding.StatusVerifyError, result.Status) - assert.Contains(t, result.Message, "500") + assert.Zero(t, requests.Load()) } -func TestVerify_Type_ReturnsCorrectID(t *testing.T) { - v := &Verifier{} - assert.Equal(t, "newrelic-api-key", v.Type()) +func TestVerificationRequestBudget(t *testing.T) { + assert.Equal(t, len(officialEndpoints), (&Verifier{}).VerificationRequestBudget()) + assert.Equal(t, 1, (&Verifier{endpoints: []endpoint{{region: "test", url: "https://example.test"}}}).VerificationRequestBudget()) } -func TestVerify_EmptyToken_ReturnsUnverified(t *testing.T) { - v := &Verifier{} - - raw := detector.RawFinding{ - DetectorID: detectorID, - Raw: []byte(""), - Redacted: "", - } - - result := v.Verify(context.Background(), raw) - - assert.Equal(t, finding.StatusUnverified, result.Status) - assert.Equal(t, "empty token", result.Message) +func TestVerify_Type_ReturnsCorrectID(t *testing.T) { + assert.Equal(t, detectorID, (&Verifier{}).Type()) } diff --git a/internal/verifier/notion/notion_shared_test.go b/internal/verifier/notion/notion_shared_test.go new file mode 100644 index 0000000..7333c5b --- /dev/null +++ b/internal/verifier/notion/notion_shared_test.go @@ -0,0 +1,16 @@ +package notion + +import ( + "net/http" + "testing" + + "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/internal/verifier" + "github.com/HodeTech/leakwatch/internal/verifier/internal/vtest" +) + +func TestVerify_SharedSafetySuite(t *testing.T) { + vtest.Run(t, vtest.Case{Name: "notion", New: func(url string, client *http.Client) verifier.Verifier { + return &Verifier{apiURL: url, httpClient: client} + }, Raw: detector.RawFinding{DetectorID: detectorID, Raw: []byte("ntn_SyntheticNotionCredentialFixture123456789")}}) +} diff --git a/internal/verifier/okta/okta_shared_test.go b/internal/verifier/okta/okta_shared_test.go new file mode 100644 index 0000000..337c170 --- /dev/null +++ b/internal/verifier/okta/okta_shared_test.go @@ -0,0 +1,16 @@ +package okta + +import ( + "net/http" + "testing" + + "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/internal/verifier" + "github.com/HodeTech/leakwatch/internal/verifier/internal/vtest" +) + +func TestVerify_SharedSafetySuite(t *testing.T) { + vtest.Run(t, vtest.Case{Name: "okta", New: func(url string, client *http.Client) verifier.Verifier { + return &Verifier{apiURL: url, httpClient: client} + }, Raw: detector.RawFinding{DetectorID: detectorID, Raw: []byte("00SyntheticOktaCredentialFixture1234567890")}}) +} diff --git a/internal/verifier/okta/okta_verifier_test.go b/internal/verifier/okta/okta_verifier_test.go index 97734b7..b9a72da 100644 --- a/internal/verifier/okta/okta_verifier_test.go +++ b/internal/verifier/okta/okta_verifier_test.go @@ -2,17 +2,45 @@ package okta import ( "context" + "io" "net/http" "net/http/httptest" + "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/HodeTech/leakwatch/internal/detector" + oktadetector "github.com/HodeTech/leakwatch/internal/detector/okta" + "github.com/HodeTech/leakwatch/internal/detector/testutil" "github.com/HodeTech/leakwatch/pkg/finding" ) +func TestVerify_RealDetectorOutput_UsesOktaDomain(t *testing.T) { + fixture := testutil.RegisteredDetectorFixtures()[detectorID] + findings := testutil.ScanViaMatcher(&oktadetector.Detector{}, fixture.Input) + require.Len(t, findings, 1) + wantDomain := findings[0].ExtraData["domain"] + require.NotEmpty(t, wantDomain) + + var gotURL string + client := &http.Client{Transport: oktaRoundTrip(func(r *http.Request) (*http.Response, error) { + gotURL = r.URL.String() + return &http.Response{ + StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(strings.NewReader(`{"profile":{"login":"fixture@example.com"}}`)), Request: r, + }, nil + })} + result := (&Verifier{httpClient: client}).Verify(t.Context(), findings[0]) + require.Equal(t, finding.StatusVerifiedActive, result.Status) + assert.Equal(t, "https://"+wantDomain+"/api/v1/users/me", gotURL) +} + +type oktaRoundTrip func(*http.Request) (*http.Response, error) + +func (f oktaRoundTrip) RoundTrip(request *http.Request) (*http.Response, error) { return f(request) } + func TestVerify_ValidToken_ReturnsActive(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "/api/v1/users/me", r.URL.Path) diff --git a/internal/verifier/postmark/postmark_shared_test.go b/internal/verifier/postmark/postmark_shared_test.go new file mode 100644 index 0000000..a5a8e60 --- /dev/null +++ b/internal/verifier/postmark/postmark_shared_test.go @@ -0,0 +1,16 @@ +package postmark + +import ( + "net/http" + "testing" + + "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/internal/verifier" + "github.com/HodeTech/leakwatch/internal/verifier/internal/vtest" +) + +func TestVerify_SharedSafetySuite(t *testing.T) { + vtest.Run(t, vtest.Case{Name: "postmark", New: func(url string, client *http.Client) verifier.Verifier { + return &Verifier{apiURL: url, httpClient: client} + }, Raw: detector.RawFinding{DetectorID: detectorID, Raw: []byte("abcdef01-2345-6789-abcd-0123456789ab")}}) +} diff --git a/internal/verifier/pypi/pypi_shared_test.go b/internal/verifier/pypi/pypi_shared_test.go new file mode 100644 index 0000000..9c018a4 --- /dev/null +++ b/internal/verifier/pypi/pypi_shared_test.go @@ -0,0 +1,16 @@ +package pypi + +import ( + "net/http" + "testing" + + "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/internal/verifier" + "github.com/HodeTech/leakwatch/internal/verifier/internal/vtest" +) + +func TestVerify_SharedSafetySuite(t *testing.T) { + vtest.Run(t, vtest.Case{Name: "pypi", New: func(url string, client *http.Client) verifier.Verifier { + return &Verifier{apiURL: url, httpClient: client} + }, Raw: detector.RawFinding{DetectorID: detectorID, Raw: []byte("pypi-SyntheticCredentialFixture1234567890")}, SkipMalformed: true}) +} diff --git a/internal/verifier/registry.go b/internal/verifier/registry.go index dfcc876..6997ce8 100644 --- a/internal/verifier/registry.go +++ b/internal/verifier/registry.go @@ -14,12 +14,19 @@ var ( // Each verifier package calls this function in its init() function. // Panics if a verifier with the same Type() is already registered. func Register(v Verifier) { + if v == nil || isNilVerifier(v) { + panic("cannot register a nil verifier") + } + typeID := v.Type() + if typeID == "" { + panic("cannot register a verifier with an empty type") + } mu.Lock() defer mu.Unlock() - if _, exists := verifiers[v.Type()]; exists { - panic("duplicate verifier type: " + v.Type()) + if _, exists := verifiers[typeID]; exists { + panic("duplicate verifier type: " + typeID) } - verifiers[v.Type()] = v + verifiers[typeID] = v } // Get returns the verifier registered for the given detector ID. diff --git a/internal/verifier/registry_test.go b/internal/verifier/registry_test.go index e80ea2e..6037f70 100644 --- a/internal/verifier/registry_test.go +++ b/internal/verifier/registry_test.go @@ -51,6 +51,15 @@ func TestRegister_DuplicateType_Panics(t *testing.T) { }) } +func TestRegister_NilOrEmptyType_PanicsWithoutMutatingRegistry(t *testing.T) { + Reset() + var typedNil *mockVerifier + assert.Panics(t, func() { Register(nil) }) + assert.Panics(t, func() { Register(typedNil) }) + assert.Panics(t, func() { Register(newMock("")) }) + assert.Empty(t, All()) +} + func TestGet_UnregisteredType_ReturnsFalse(t *testing.T) { t.Cleanup(func() { Reset() }) diff --git a/internal/verifier/rubygems/rubygems_shared_test.go b/internal/verifier/rubygems/rubygems_shared_test.go new file mode 100644 index 0000000..7fd5bff --- /dev/null +++ b/internal/verifier/rubygems/rubygems_shared_test.go @@ -0,0 +1,16 @@ +package rubygems + +import ( + "net/http" + "testing" + + "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/internal/verifier" + "github.com/HodeTech/leakwatch/internal/verifier/internal/vtest" +) + +func TestVerify_SharedSafetySuite(t *testing.T) { + vtest.Run(t, vtest.Case{Name: "rubygems", New: func(url string, client *http.Client) verifier.Verifier { + return &Verifier{apiURL: url, httpClient: client} + }, Raw: detector.RawFinding{DetectorID: detectorID, Raw: []byte("rubygems_abcdef0123456789abcdef0123456789")}, SkipMalformed: true}) +} diff --git a/internal/verifier/shopify/shopify_shared_test.go b/internal/verifier/shopify/shopify_shared_test.go new file mode 100644 index 0000000..96af353 --- /dev/null +++ b/internal/verifier/shopify/shopify_shared_test.go @@ -0,0 +1,16 @@ +package shopify + +import ( + "net/http" + "testing" + + "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/internal/verifier" + "github.com/HodeTech/leakwatch/internal/verifier/internal/vtest" +) + +func TestVerify_SharedSafetySuite(t *testing.T) { + vtest.Run(t, vtest.Case{Name: "shopify", New: func(url string, client *http.Client) verifier.Verifier { + return &Verifier{apiURL: url, httpClient: client} + }, Raw: detector.RawFinding{DetectorID: detectorID, Raw: []byte("shpat_abcdef0123456789abcdef0123456789")}}) +} diff --git a/internal/verifier/shopify/shopify_verifier.go b/internal/verifier/shopify/shopify_verifier.go index c368175..bc8e069 100644 --- a/internal/verifier/shopify/shopify_verifier.go +++ b/internal/verifier/shopify/shopify_verifier.go @@ -1,13 +1,15 @@ -// Package shopify provides a verifier for Shopify access tokens. -// It uses the Shopify Admin API GET /admin/api/2024-01/shop.json endpoint -// to check token validity. A store domain is required via ExtraData. +// Package shopify provides fail-closed verification for Shopify Admin API +// access tokens against an operator-trusted store origin. package shopify import ( "context" "encoding/json" + "errors" "io" "net/http" + "net/url" + "strings" "github.com/HodeTech/leakwatch/internal/detector" "github.com/HodeTech/leakwatch/internal/verifier" @@ -15,79 +17,105 @@ import ( "github.com/HodeTech/leakwatch/pkg/finding" ) -const detectorID = "shopify-access-token" - -// shopAPIPath is the Shopify Admin API endpoint for shop details. -const shopAPIPath = "/admin/api/2024-01/shop.json" +const ( + detectorID = "shopify-access-token" + // Shopify versions are supported for at least 12 months. This pin is + // reviewed alongside LastContractReviewedAt in the capability manifest. + shopifyAPIVersion = "2026-07" + shopAPIPath = "/admin/api/" + shopifyAPIVersion + "/graphql.json" + shopIdentityQuery = `{"query":"query LeakwatchVerify { shop { name } }"}` +) -// Verifier checks whether a Shopify access token is active by calling the -// Shopify Admin API. It NEVER logs or persists raw token values. +// Verifier checks a token only when apiURL has been set from trusted operator +// configuration. Finding-controlled store_domain metadata is never used for +// routing, preventing wrong-issuer false-inactive results and credential +// forwarding to an untrusted host. type Verifier struct { - // apiURL overrides the full base URL (for testing). When set, store_domain - // from ExtraData is ignored and apiURL is used directly. - apiURL string - // httpClient overrides the default HTTP client (for testing). + apiURL string httpClient *http.Client } +// NewForTrustedInstance accepts only an operator-selected canonical +// myshopify.com store origin. Custom domains and repository metadata are not +// routing authority for Admin API credentials. +func NewForTrustedInstance(instanceURL string) (*Verifier, error) { + normalized, err := verifier.NormalizeTrustedHTTPSOrigin(instanceURL) + if err != nil { + return nil, err + } + u, err := url.Parse(normalized) + if err != nil || u.Port() != "" || !strings.HasSuffix(u.Hostname(), ".myshopify.com") || + strings.TrimSuffix(u.Hostname(), ".myshopify.com") == "" { + return nil, errors.New("invalid Shopify store origin: a concrete myshopify.com store is required") + } + return &Verifier{apiURL: normalized}, nil +} + +// WithTrustedInstance implements verifier.TrustedInstanceConfigurer. +func (*Verifier) WithTrustedInstance(instanceURL string) (verifier.Verifier, error) { + return NewForTrustedInstance(instanceURL) +} + func init() { verifier.Register(&Verifier{}) } // Type returns the detector ID this verifier handles. -func (v *Verifier) Type() string { - return detectorID -} +func (v *Verifier) Type() string { return detectorID } -// Verify checks if the detected Shopify access token is valid/active. -// Raw contains the token value. ExtraData["store_domain"] must contain the -// Shopify store domain (e.g., "mystore.myshopify.com"). +// Verify performs a read-only GraphQL shop identity query. Only HTTP 401 on +// the trusted store is definitive inactivity; authorization and GraphQL errors +// remain verify_error. func (v *Verifier) Verify(ctx context.Context, raw detector.RawFinding) finding.VerificationResult { token := string(raw.Raw) if token == "" { + return finding.VerificationResult{Status: finding.StatusUnverified, Message: "empty token"} + } + if v.apiURL == "" { return finding.VerificationResult{ Status: finding.StatusUnverified, - Message: "empty token", + Message: "trusted Shopify store origin is not configured", } } - baseURL := v.apiURL - if baseURL == "" { - domain := raw.ExtraData["store_domain"] - if domain == "" { - return finding.VerificationResult{ - Status: finding.StatusUnverified, - Message: "store domain required for verification", - } - } - baseURL = "https://" + domain - } - return httpx.VerifyToken(ctx, v.httpClient, token, httpx.TokenSpec{ Name: "shopify", Request: httpx.Request{ - URL: baseURL + shopAPIPath, - Header: map[string]string{"X-Shopify-Access-Token": token}, + Method: http.MethodPost, + URL: v.apiURL + shopAPIPath, + Body: []byte(shopIdentityQuery), + Header: map[string]string{ + "Accept": "application/json", + "Content-Type": "application/json", + "X-Shopify-Access-Token": token, + }, }, - ActiveMessage: "Shopify access token is active", - InactiveMessage: "Shopify access token is invalid or revoked", - Decode: decodeShop, + InactiveStatuses: []int{http.StatusUnauthorized}, + ActiveMessage: "Shopify access token is active on the trusted store", + InactiveMessage: "Shopify access token is invalid or revoked on the trusted store", + Decode: decodeShopIdentity, + RequireCompleteBody: true, + RequireJSONContentType: true, }) } -// decodeShop reports the shop name as shop_name when present. -func decodeShop(body io.Reader) (map[string]string, string, error) { - var shopResp struct { - Shop struct { - Name string `json:"name"` - } `json:"shop"` +func decodeShopIdentity(body io.Reader) (map[string]string, string, error) { + var response struct { + Data *struct { + Shop *struct { + Name string `json:"name"` + } `json:"shop"` + } `json:"data"` + Errors []json.RawMessage `json:"errors"` } - if err := json.NewDecoder(body).Decode(&shopResp); err != nil { + if err := json.NewDecoder(body).Decode(&response); err != nil { return nil, "", err } - extra := map[string]string{} - if shopResp.Shop.Name != "" { - extra["shop_name"] = shopResp.Shop.Name + if len(response.Errors) > 0 { + return nil, "", errors.New("shopify GraphQL response contains errors") + } + if response.Data == nil || response.Data.Shop == nil || response.Data.Shop.Name == "" { + return nil, "", errors.New("shopify GraphQL response is missing shop identity") } - return extra, "", nil + return map[string]string{"shop_name": response.Data.Shop.Name}, "", nil } diff --git a/internal/verifier/shopify/shopify_verifier_test.go b/internal/verifier/shopify/shopify_verifier_test.go index cf6ccfd..5ffb1f8 100644 --- a/internal/verifier/shopify/shopify_verifier_test.go +++ b/internal/verifier/shopify/shopify_verifier_test.go @@ -2,159 +2,157 @@ package shopify import ( "context" + "io" "net/http" "net/http/httptest" + "strings" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/HodeTech/leakwatch/internal/detector" + shopifydetector "github.com/HodeTech/leakwatch/internal/detector/shopify" "github.com/HodeTech/leakwatch/pkg/finding" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestVerify_ValidToken_ReturnsActive(t *testing.T) { +func shopifyFinding() detector.RawFinding { + return detector.RawFinding{ + DetectorID: detectorID, + Raw: []byte("shpat_" + strings.Repeat("ab12cd34", 4)), + Redacted: "shpat_****cd34", + } +} + +func TestVerify_TrustedStoreUsesCurrentGraphQLContract(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, shopAPIPath, r.URL.Path) - assert.NotEmpty(t, r.Header.Get("X-Shopify-Access-Token")) + assert.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, "/admin/api/2026-07/graphql.json", r.URL.Path) + assert.Equal(t, "application/json", r.Header.Get("Content-Type")) + assert.Equal(t, "application/json", r.Header.Get("Accept")) + assert.Equal(t, string(shopifyFinding().Raw), r.Header.Get("X-Shopify-Access-Token")) + body, err := io.ReadAll(r.Body) + require.NoError(t, err) + assert.JSONEq(t, shopIdentityQuery, string(body)) w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(`{"shop":{"name":"My Test Store"}}`)) + _, _ = w.Write([]byte(`{"data":{"shop":{"name":"My Test Store"}}}`)) })) defer server.Close() - v := &Verifier{ - apiURL: server.URL, - httpClient: server.Client(), - } - - raw := detector.RawFinding{ - DetectorID: detectorID, - Raw: []byte("shpat_abcdef1234567890abcdef1234567890"), - Redacted: "shpat_****7890", - } - - result := v.Verify(context.Background(), raw) + result := (&Verifier{apiURL: server.URL, httpClient: server.Client()}).Verify( + context.Background(), shopifyFinding(), + ) require.Equal(t, finding.StatusVerifiedActive, result.Status) - assert.Equal(t, "Shopify access token is active", result.Message) + assert.Equal(t, "Shopify access token is active on the trusted store", result.Message) assert.Equal(t, "My Test Store", result.ExtraData["shop_name"]) } -func TestVerify_InvalidToken_ReturnsInactive(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusUnauthorized) - _, _ = w.Write([]byte(`{"errors":"[API] Invalid API key or access token"}`)) +func TestVerify_WithoutTrustedStoreMakesNoRequest(t *testing.T) { + requests := 0 + server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { + requests++ })) defer server.Close() - v := &Verifier{ - apiURL: server.URL, - httpClient: server.Client(), - } + raw := shopifyFinding() + raw.ExtraData = map[string]string{"store_domain": strings.TrimPrefix(server.URL, "http://")} + result := (&Verifier{httpClient: server.Client()}).Verify(context.Background(), raw) - raw := detector.RawFinding{ - DetectorID: detectorID, - Raw: []byte("shpat_invalidtoken1234567890abcdef1234"), - Redacted: "shpat_****1234", - } - - result := v.Verify(context.Background(), raw) - - assert.Equal(t, finding.StatusVerifiedInactive, result.Status) - assert.Equal(t, "Shopify access token is invalid or revoked", result.Message) + assert.Equal(t, finding.StatusUnverified, result.Status) + assert.Equal(t, "trusted Shopify store origin is not configured", result.Message) + assert.Zero(t, requests, "finding-controlled store_domain must never select a request target") } -func TestVerify_ServerError_ReturnsError(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - _, _ = w.Write([]byte(`{"errors":"Internal server error"}`)) - })) - defer server.Close() - - v := &Verifier{ - apiURL: server.URL, - httpClient: server.Client(), +func TestNewForTrustedInstance_AcceptsOnlyCanonicalShopifyStore(t *testing.T) { + configured, err := NewForTrustedInstance("https://fixture-store.myshopify.com/") + require.NoError(t, err) + assert.Equal(t, "https://fixture-store.myshopify.com", configured.apiURL) + for _, origin := range []string{ + "https://myshopify.com", "https://fixture-store.myshopify.com.attacker.example", + "https://fixture-store.myshopify.com:443", "https://custom-store.example", + } { + _, err := NewForTrustedInstance(origin) + assert.Error(t, err, origin) } - - raw := detector.RawFinding{ - DetectorID: detectorID, - Raw: []byte("shpat_sometoken12345678901234567890abcd"), - Redacted: "shpat_****abcd", - } - - result := v.Verify(context.Background(), raw) - - assert.Equal(t, finding.StatusVerifyError, result.Status) - assert.Contains(t, result.Message, "500") -} - -func TestVerify_Type_ReturnsCorrectID(t *testing.T) { - v := &Verifier{} - assert.Equal(t, "shopify-access-token", v.Type()) } -func TestVerify_EmptyToken_ReturnsUnverified(t *testing.T) { - v := &Verifier{} - - raw := detector.RawFinding{ - DetectorID: detectorID, - Raw: []byte(""), - Redacted: "", - } - - result := v.Verify(context.Background(), raw) +func TestVerify_RealDetectorFindingMatchesRequiresContextCapability(t *testing.T) { + findings := (&shopifydetector.Detector{}).Scan(context.Background(), shopifyFinding().Raw) + require.Len(t, findings, 1) + result := (&Verifier{}).Verify(context.Background(), findings[0]) assert.Equal(t, finding.StatusUnverified, result.Status) - assert.Equal(t, "empty token", result.Message) + assert.Equal(t, "trusted Shopify store origin is not configured", result.Message) } -func TestVerify_NoDomain_ReturnsUnverified(t *testing.T) { - v := &Verifier{} - - raw := detector.RawFinding{ - DetectorID: detectorID, - Raw: []byte("shpat_abcdef1234567890abcdef1234567890"), - Redacted: "shpat_****7890", - ExtraData: map[string]string{}, +func TestVerify_Only401IsInactive(t *testing.T) { + tests := []struct { + name string + status int + want finding.VerificationStatus + }{ + {name: "401 authentication rejection", status: http.StatusUnauthorized, want: finding.StatusVerifiedInactive}, + {name: "403 authorization rejection", status: http.StatusForbidden, want: finding.StatusVerifyError}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(tc.status) + })) + defer server.Close() + + result := (&Verifier{apiURL: server.URL, httpClient: server.Client()}).Verify( + context.Background(), shopifyFinding(), + ) + assert.Equal(t, tc.want, result.Status) + }) } - - result := v.Verify(context.Background(), raw) - - assert.Equal(t, finding.StatusUnverified, result.Status) - assert.Equal(t, "store domain required for verification", result.Message) } -func TestVerify_WithDomain_UsesStoreDomain(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, shopAPIPath, r.URL.Path) - assert.NotEmpty(t, r.Header.Get("X-Shopify-Access-Token")) +func TestVerify_MalformedOrGraphQLErrorResponseIsVerifyError(t *testing.T) { + tests := []struct { + name string + contentType string + body string + }{ + {name: "missing data", contentType: "application/json", body: `{}`}, + {name: "null shop", contentType: "application/json", body: `{"data":{"shop":null}}`}, + {name: "GraphQL errors", contentType: "application/json", body: `{"errors":[{"message":"denied"}]}`}, + {name: "wrong content type", contentType: "text/html", body: `{"data":{"shop":{"name":"Store"}}}`}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", tc.contentType) + _, _ = w.Write([]byte(tc.body)) + })) + defer server.Close() + + result := (&Verifier{apiURL: server.URL, httpClient: server.Client()}).Verify( + context.Background(), shopifyFinding(), + ) + assert.Equal(t, finding.StatusVerifyError, result.Status) + }) + } +} - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(`{"shop":{"name":"Domain Store"}}`)) +func TestVerify_ServerErrorReturnsError(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) })) defer server.Close() - // Use apiURL to simulate domain-based URL construction in tests. - v := &Verifier{ - apiURL: server.URL, - httpClient: server.Client(), - } - - raw := detector.RawFinding{ - DetectorID: detectorID, - Raw: []byte("shpat_abcdef1234567890abcdef1234567890"), - Redacted: "shpat_****7890", - ExtraData: map[string]string{ - "store_domain": "mystore.myshopify.com", - }, - } - - result := v.Verify(context.Background(), raw) + result := (&Verifier{apiURL: server.URL, httpClient: server.Client()}).Verify( + context.Background(), shopifyFinding(), + ) + assert.Equal(t, finding.StatusVerifyError, result.Status) +} - require.Equal(t, finding.StatusVerifiedActive, result.Status) - assert.Equal(t, "Shopify access token is active", result.Message) - assert.Equal(t, "Domain Store", result.ExtraData["shop_name"]) +func TestVerify_TypeAndEmptyToken(t *testing.T) { + assert.Equal(t, detectorID, (&Verifier{}).Type()) + result := (&Verifier{}).Verify(context.Background(), detector.RawFinding{}) + assert.Equal(t, finding.StatusUnverified, result.Status) + assert.Equal(t, "empty token", result.Message) } diff --git a/internal/verifier/snowflake/snowflake_verifier_test.go b/internal/verifier/snowflake/snowflake_verifier_test.go index 5e09974..ca04fdb 100644 --- a/internal/verifier/snowflake/snowflake_verifier_test.go +++ b/internal/verifier/snowflake/snowflake_verifier_test.go @@ -7,9 +7,22 @@ import ( "github.com/stretchr/testify/assert" "github.com/HodeTech/leakwatch/internal/detector" + snowflakedetector "github.com/HodeTech/leakwatch/internal/detector/snowflake" + "github.com/HodeTech/leakwatch/internal/detector/testutil" "github.com/HodeTech/leakwatch/pkg/finding" + "github.com/stretchr/testify/require" ) +func TestVerify_RealDetectorOutput_ValidatesPairedConnection(t *testing.T) { + fixture := testutil.RegisteredDetectorFixtures()[detectorID] + findings := testutil.ScanViaMatcher(&snowflakedetector.Detector{}, fixture.Input) + require.Len(t, findings, 1) + require.NotEmpty(t, findings[0].RawV2) + result := (&Verifier{}).Verify(t.Context(), findings[0]) + assert.Equal(t, finding.StatusUnverified, result.Status) + assert.Contains(t, result.Message, "format valid") +} + func TestVerifier_Type_ReturnsCorrectID(t *testing.T) { v := &Verifier{} assert.Equal(t, "snowflake-credentials", v.Type()) diff --git a/internal/verifier/snyk/snyk_verifier.go b/internal/verifier/snyk/snyk_verifier.go index 04d2598..21440b1 100644 --- a/internal/verifier/snyk/snyk_verifier.go +++ b/internal/verifier/snyk/snyk_verifier.go @@ -4,8 +4,12 @@ package snyk import ( "context" + "encoding/json" + "fmt" + "io" "net/http" "net/url" + "strings" "github.com/HodeTech/leakwatch/internal/detector" "github.com/HodeTech/leakwatch/internal/verifier" @@ -15,22 +19,36 @@ import ( const detectorID = "snyk-api-key" -// defaultAPIURL is the base URL for the Snyk API. -const defaultAPIURL = "https://api.snyk.io" - // apiVersion is the Snyk REST API version. The REST API mandates a // ?version=YYYY-MM-DD query parameter; omitting it makes the API respond 400. -const apiVersion = "2024-04-29" +const apiVersion = "2024-10-15" // Verifier checks whether a Snyk API key is active by calling the // Snyk REST API. It NEVER logs or persists raw key values. type Verifier struct { - // apiURL overrides the Snyk API base URL (for testing). + // apiURL is reserved for a validated, trusted Snyk API origin. The + // production registration leaves it empty until operator wiring exists; + // tests inject a local server. apiURL string // httpClient overrides the default HTTP client (for testing). httpClient *http.Client } +// NewForTrustedInstance constructs a Snyk verifier for an explicit regional, +// government, or operator-controlled private Snyk API origin. +func NewForTrustedInstance(instanceURL string) (*Verifier, error) { + normalized, err := verifier.NormalizeTrustedHTTPSOrigin(instanceURL) + if err != nil { + return nil, err + } + return &Verifier{apiURL: normalized}, nil +} + +// WithTrustedInstance implements verifier.TrustedInstanceConfigurer. +func (*Verifier) WithTrustedInstance(instanceURL string) (verifier.Verifier, error) { + return NewForTrustedInstance(instanceURL) +} + func init() { verifier.Register(&Verifier{}) } @@ -44,7 +62,16 @@ func (v *Verifier) Type() string { // Raw contains the key value. func (v *Verifier) Verify(ctx context.Context, raw detector.RawFinding) finding.VerificationResult { token := string(raw.Raw) - apiURL := httpx.BaseURL(v.apiURL, defaultAPIURL) + if token == "" { + return finding.VerificationResult{Status: finding.StatusUnverified, Message: "empty token"} + } + if v.apiURL == "" { + return finding.VerificationResult{ + Status: finding.StatusUnverified, + Message: "trusted Snyk API origin is not configured", + } + } + apiURL := v.apiURL // The Snyk REST API requires the version as a query parameter; without it // the live API returns 400. The Version header is kept for compatibility. @@ -59,8 +86,44 @@ func (v *Verifier) Verify(ctx context.Context, raw detector.RawFinding) finding. "Version": apiVersion, }, }, - InactiveStatuses: []int{http.StatusUnauthorized, http.StatusForbidden}, - ActiveMessage: "Snyk API key is active", - InactiveMessage: "Snyk API key is invalid or revoked", + InactiveStatuses: []int{http.StatusUnauthorized}, + ActiveMessage: "Snyk API key is active", + InactiveMessage: "Snyk API key is invalid or revoked", + Decode: decodeSelf, + DecodeInactive: decodeInactiveResponse, + RequireCompleteBody: true, + RequireJSONContentType: true, }) } + +func decodeInactiveResponse(body io.Reader) error { + var response struct { + Code int `json:"code"` + Message string `json:"message"` + } + decoder := json.NewDecoder(body) + if err := decoder.Decode(&response); err != nil { + return err + } + if response.Code != http.StatusUnauthorized || + strings.ToLower(strings.TrimSpace(response.Message)) != "invalid auth token provided" { + return fmt.Errorf("unexpected Snyk authentication error") + } + if decoder.Decode(&struct{}{}) != io.EOF { + return fmt.Errorf("unexpected trailing Snyk response data") + } + return nil +} + +func decodeSelf(body io.Reader) (map[string]string, string, error) { + var response struct { + Data json.RawMessage `json:"data"` + } + if err := json.NewDecoder(body).Decode(&response); err != nil { + return nil, "", err + } + if len(response.Data) == 0 || string(response.Data) == "null" { + return nil, "", fmt.Errorf("missing Snyk self data") + } + return nil, "", nil +} diff --git a/internal/verifier/snyk/snyk_verifier_test.go b/internal/verifier/snyk/snyk_verifier_test.go index 904289c..52d6edf 100644 --- a/internal/verifier/snyk/snyk_verifier_test.go +++ b/internal/verifier/snyk/snyk_verifier_test.go @@ -17,9 +17,9 @@ func TestVerify_ValidKey_ReturnsActive(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "/rest/self", r.URL.Path) // The Snyk REST API requires the version as a query parameter. - assert.Equal(t, "2024-04-29", r.URL.Query().Get("version")) + assert.Equal(t, "2024-10-15", r.URL.Query().Get("version")) assert.Equal(t, "token test-snyk-api-key-abcdef1234567890", r.Header.Get("Authorization")) - assert.Equal(t, "2024-04-29", r.Header.Get("Version")) + assert.Equal(t, "2024-10-15", r.Header.Get("Version")) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) @@ -46,6 +46,7 @@ func TestVerify_ValidKey_ReturnsActive(t *testing.T) { func TestVerify_InvalidKey_ReturnsInactive(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"code":401,"message":"Invalid auth token provided"}`)) })) @@ -68,7 +69,26 @@ func TestVerify_InvalidKey_ReturnsInactive(t *testing.T) { assert.Equal(t, "Snyk API key is invalid or revoked", result.Message) } -func TestVerify_ForbiddenKey_ReturnsInactive(t *testing.T) { +func TestVerify_InactiveResponseMustBeDefinitiveJSON(t *testing.T) { + tests := []struct{ contentType, body string }{ + {contentType: "text/html", body: `login`}, + {contentType: "application/json", body: `{"code":401,"message":"challenge"}`}, + } + for _, tc := range tests { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", tc.contentType) + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(tc.body)) + })) + result := (&Verifier{apiURL: server.URL, httpClient: server.Client()}).Verify( + context.Background(), detector.RawFinding{Raw: []byte("synthetic-snyk-key")}, + ) + server.Close() + assert.Equal(t, finding.StatusVerifyError, result.Status) + } +} + +func TestVerify_ForbiddenKey_ReturnsVerifyError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusForbidden) _, _ = w.Write([]byte(`{"code":403,"message":"Forbidden"}`)) @@ -88,8 +108,35 @@ func TestVerify_ForbiddenKey_ReturnsInactive(t *testing.T) { result := v.Verify(context.Background(), raw) - assert.Equal(t, finding.StatusVerifiedInactive, result.Status) - assert.Equal(t, "Snyk API key is invalid or revoked", result.Message) + assert.Equal(t, finding.StatusVerifyError, result.Status) + assert.Contains(t, result.Message, "403") +} + +func TestVerify_MalformedSuccessResponse_ReturnsVerifyError(t *testing.T) { + tests := []struct { + name string + contentType string + body string + }{ + {name: "missing data", contentType: "application/vnd.api+json", body: `{}`}, + {name: "null data", contentType: "application/json", body: `{"data":null}`}, + {name: "wrong content type", contentType: "text/plain", body: `{"data":{}}`}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", tc.contentType) + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(tc.body)) + })) + defer server.Close() + + result := (&Verifier{apiURL: server.URL, httpClient: server.Client()}).Verify( + context.Background(), detector.RawFinding{Raw: []byte("synthetic-snyk-key")}, + ) + assert.Equal(t, finding.StatusVerifyError, result.Status) + }) + } } func TestVerify_ServerError_ReturnsError(t *testing.T) { @@ -135,3 +182,21 @@ func TestVerify_EmptyToken_ReturnsUnverified(t *testing.T) { assert.Equal(t, finding.StatusUnverified, result.Status) assert.Equal(t, "empty token", result.Message) } + +func TestVerify_WithoutTrustedOrigin_MakesNoRequest(t *testing.T) { + v := &Verifier{} + result := v.Verify(context.Background(), detector.RawFinding{Raw: []byte("synthetic-snyk-key")}) + + assert.Equal(t, finding.StatusUnverified, result.Status) + assert.Equal(t, "trusted Snyk API origin is not configured", result.Message) +} + +func TestNewForTrustedInstance_ValidatesOrigin(t *testing.T) { + configured, err := NewForTrustedInstance("https://api.eu.snyk.io/") + require.NoError(t, err) + assert.Equal(t, "https://api.eu.snyk.io", configured.apiURL) + for _, origin := range []string{"http://api.snyk.io", "https://127.0.0.1", "https://localhost", "https://api.snyk.io/rest"} { + _, err := NewForTrustedInstance(origin) + assert.Error(t, err, origin) + } +} diff --git a/internal/verifier/sonarcloud/sonarcloud_shared_test.go b/internal/verifier/sonarcloud/sonarcloud_shared_test.go new file mode 100644 index 0000000..3ab6b4c --- /dev/null +++ b/internal/verifier/sonarcloud/sonarcloud_shared_test.go @@ -0,0 +1,16 @@ +package sonarcloud + +import ( + "net/http" + "testing" + + "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/internal/verifier" + "github.com/HodeTech/leakwatch/internal/verifier/internal/vtest" +) + +func TestVerify_SharedSafetySuite(t *testing.T) { + vtest.Run(t, vtest.Case{Name: "sonarcloud", New: func(url string, client *http.Client) verifier.Verifier { + return &Verifier{apiURL: url, httpClient: client} + }, Raw: detector.RawFinding{DetectorID: detectorID, Raw: []byte("sqp_abcdef0123456789abcdef0123456789abcdef01")}}) +} diff --git a/internal/verifier/supabase/supabase_shared_test.go b/internal/verifier/supabase/supabase_shared_test.go new file mode 100644 index 0000000..4c40f31 --- /dev/null +++ b/internal/verifier/supabase/supabase_shared_test.go @@ -0,0 +1,16 @@ +package supabase + +import ( + "net/http" + "testing" + + "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/internal/verifier" + "github.com/HodeTech/leakwatch/internal/verifier/internal/vtest" +) + +func TestVerify_SharedSafetySuite(t *testing.T) { + vtest.Run(t, vtest.Case{Name: "supabase", New: func(url string, client *http.Client) verifier.Verifier { + return &Verifier{apiURL: url, httpClient: client} + }, Raw: detector.RawFinding{DetectorID: detectorID, Raw: []byte("sbp_abcdef0123456789abcdef0123456789abcdef01")}, SkipMalformed: true}) +} diff --git a/internal/verifier/supabase/supabase_verifier.go b/internal/verifier/supabase/supabase_verifier.go index a669598..57c522b 100644 --- a/internal/verifier/supabase/supabase_verifier.go +++ b/internal/verifier/supabase/supabase_verifier.go @@ -1,5 +1,5 @@ -// Package supabase provides a verifier for Supabase service keys. -// It uses the Supabase Management API GET /v1/projects endpoint to check key validity. +// Package supabase provides a verifier for Supabase personal access tokens. It +// uses the Supabase Management API GET /v1/projects endpoint to check validity. package supabase import ( @@ -17,8 +17,8 @@ const detectorID = "supabase-service-key" // defaultAPIURL is the base URL for the Supabase Management API. const defaultAPIURL = "https://api.supabase.com" -// Verifier checks whether a Supabase service key is active by calling the -// Supabase Management API. It NEVER logs or persists raw key values. +// Verifier checks whether a Supabase personal access token is active by calling +// the Supabase Management API. It NEVER logs or persists raw token values. type Verifier struct { // apiURL overrides the Supabase API base URL (for testing). apiURL string @@ -35,7 +35,7 @@ func (v *Verifier) Type() string { return detectorID } -// Verify checks if the detected Supabase service key is valid/active. +// Verify checks if the detected Supabase personal access token is valid/active. // Raw contains the key value. func (v *Verifier) Verify(ctx context.Context, raw detector.RawFinding) finding.VerificationResult { token := string(raw.Raw) @@ -47,7 +47,7 @@ func (v *Verifier) Verify(ctx context.Context, raw detector.RawFinding) finding. URL: apiURL + "/v1/projects", Header: map[string]string{"Authorization": "Bearer " + token}, }, - ActiveMessage: "Supabase service key is active", - InactiveMessage: "Supabase service key is invalid or revoked", + ActiveMessage: "Supabase personal access token is active", + InactiveMessage: "Supabase personal access token is invalid or revoked", }) } diff --git a/internal/verifier/supabase/supabase_verifier_test.go b/internal/verifier/supabase/supabase_verifier_test.go index c9bd28e..d1e1a76 100644 --- a/internal/verifier/supabase/supabase_verifier_test.go +++ b/internal/verifier/supabase/supabase_verifier_test.go @@ -38,7 +38,7 @@ func TestVerify_ValidKey_ReturnsActive(t *testing.T) { result := v.Verify(context.Background(), raw) require.Equal(t, finding.StatusVerifiedActive, result.Status) - assert.Equal(t, "Supabase service key is active", result.Message) + assert.Equal(t, "Supabase personal access token is active", result.Message) } func TestVerify_InvalidKey_ReturnsInactive(t *testing.T) { @@ -62,7 +62,21 @@ func TestVerify_InvalidKey_ReturnsInactive(t *testing.T) { result := v.Verify(context.Background(), raw) assert.Equal(t, finding.StatusVerifiedInactive, result.Status) - assert.Equal(t, "Supabase service key is invalid or revoked", result.Message) + assert.Equal(t, "Supabase personal access token is invalid or revoked", result.Message) +} + +func TestVerify_Forbidden_RemainsInconclusive(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusForbidden) + _, _ = w.Write([]byte(`{"message":"Forbidden"}`)) + })) + defer server.Close() + + v := &Verifier{apiURL: server.URL, httpClient: server.Client()} + result := v.Verify(context.Background(), detector.RawFinding{Raw: []byte("synthetic-sbp-token")}) + + assert.Equal(t, finding.StatusVerifyError, result.Status) + assert.NotEqual(t, finding.StatusVerifiedInactive, result.Status) } func TestVerify_ServerError_ReturnsError(t *testing.T) { diff --git a/internal/verifier/teams/teams_shared_test.go b/internal/verifier/teams/teams_shared_test.go new file mode 100644 index 0000000..bb3f08c --- /dev/null +++ b/internal/verifier/teams/teams_shared_test.go @@ -0,0 +1,23 @@ +package teams + +import ( + "net/http" + "testing" + + "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/internal/verifier" + "github.com/HodeTech/leakwatch/internal/verifier/internal/vtest" +) + +func TestVerify_SharedSafetySuite(t *testing.T) { + vtest.Run(t, vtest.Case{ + Name: "teams", + New: func(_ string, client *http.Client) verifier.Verifier { + return &Verifier{httpClient: client} + }, + RawForURL: func(endpoint string) detector.RawFinding { + return detector.RawFinding{DetectorID: detectorID, Raw: []byte(endpoint), Redacted: "https://teams.invalid/****"} + }, + SkipMalformed: true, + }) +} diff --git a/internal/verifier/teams/teams_verifier.go b/internal/verifier/teams/teams_verifier.go index 3c6439d..91d9607 100644 --- a/internal/verifier/teams/teams_verifier.go +++ b/internal/verifier/teams/teams_verifier.go @@ -36,6 +36,8 @@ type Verifier struct { httpClient *http.Client } +var _ verifier.RequestGatedVerifier = (*Verifier)(nil) + func init() { verifier.Register(&Verifier{}) } @@ -54,6 +56,26 @@ func (v *Verifier) Type() string { // - 404 Not Found -> inactive (webhook deleted or disabled) // - anything else -> unverified (cannot decide non-destructively) func (v *Verifier) Verify(ctx context.Context, raw detector.RawFinding) finding.VerificationResult { + return v.verify(ctx, raw, nil) +} + +// VerificationRequestBudget declares the single non-destructive Teams probe. +func (v *Verifier) VerificationRequestBudget() int { return 1 } + +// VerifyWithRequestGate admits the probe at its actual HTTP send point. +func (v *Verifier) VerifyWithRequestGate( + ctx context.Context, + raw detector.RawFinding, + gate verifier.RequestGate, +) finding.VerificationResult { + return v.verify(ctx, raw, gate) +} + +func (v *Verifier) verify( + ctx context.Context, + raw detector.RawFinding, + gate verifier.RequestGate, +) finding.VerificationResult { webhookURL := string(raw.Raw) if webhookURL == "" { return finding.VerificationResult{ @@ -85,6 +107,11 @@ func (v *Verifier) Verify(ctx context.Context, raw detector.RawFinding) finding. if client == nil { client = httpx.Client() } + if gate != nil { + if rejection := gate(); rejection != nil { + return *rejection + } + } resp, err := client.Do(req) if err != nil { diff --git a/internal/verifier/teams/teams_verifier_test.go b/internal/verifier/teams/teams_verifier_test.go index 32145f4..4c7d47e 100644 --- a/internal/verifier/teams/teams_verifier_test.go +++ b/internal/verifier/teams/teams_verifier_test.go @@ -7,6 +7,7 @@ import ( "net/http" "net/http/httptest" "net/url" + "sync/atomic" "testing" "github.com/stretchr/testify/assert" @@ -215,3 +216,31 @@ func TestVerify_EmptyURL_ReturnsUnverified(t *testing.T) { assert.Equal(t, finding.StatusUnverified, result.Status) assert.Equal(t, "empty webhook URL", result.Message) } + +func TestVerifyWithRequestGate_AdmitsOnlyActualTeamsSend(t *testing.T) { + t.Run("empty URL consumes no admission", func(t *testing.T) { + gateCalls := 0 + result := (&Verifier{}).VerifyWithRequestGate(context.Background(), detector.RawFinding{}, func() *finding.VerificationResult { + gateCalls++ + return nil + }) + assert.Equal(t, finding.StatusUnverified, result.Status) + assert.Zero(t, gateCalls) + }) + + t.Run("rejection prevents transport", func(t *testing.T) { + var calls atomic.Int64 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + calls.Add(1) + w.WriteHeader(http.StatusBadRequest) + })) + defer server.Close() + v := &Verifier{httpClient: server.Client()} + + result := v.VerifyWithRequestGate(context.Background(), detector.RawFinding{Raw: []byte(server.URL)}, func() *finding.VerificationResult { + return &finding.VerificationResult{Status: finding.StatusVerifyError, Message: "admission rejected"} + }) + assert.Equal(t, "admission rejected", result.Message) + assert.Zero(t, calls.Load()) + }) +} diff --git a/internal/verifier/terraform/terraform_shared_test.go b/internal/verifier/terraform/terraform_shared_test.go new file mode 100644 index 0000000..c1c4595 --- /dev/null +++ b/internal/verifier/terraform/terraform_shared_test.go @@ -0,0 +1,16 @@ +package terraform + +import ( + "net/http" + "testing" + + "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/internal/verifier" + "github.com/HodeTech/leakwatch/internal/verifier/internal/vtest" +) + +func TestVerify_SharedSafetySuite(t *testing.T) { + vtest.Run(t, vtest.Case{Name: "terraform", New: func(url string, client *http.Client) verifier.Verifier { + return &Verifier{apiURL: url, httpClient: client} + }, Raw: detector.RawFinding{DetectorID: detectorID, Raw: []byte("Ab12Cd34Ef56Gh.atlasv1.SyntheticTerraformCredentialFixture123456789")}}) +} diff --git a/internal/verifier/trusted_origin.go b/internal/verifier/trusted_origin.go new file mode 100644 index 0000000..0eee5e7 --- /dev/null +++ b/internal/verifier/trusted_origin.go @@ -0,0 +1,114 @@ +package verifier + +import ( + "fmt" + "net/netip" + "net/url" + "strconv" + "strings" +) + +// NormalizeTrustedHTTPSOrigin validates an origin explicitly supplied by the +// operator. Scanned repository content must never be passed to this function: +// accepting a finding-derived URL would turn verification into a credential +// exfiltration primitive. +func NormalizeTrustedHTTPSOrigin(raw string) (string, error) { + u, err := parseTrustedHTTPSOrigin(raw) + if err != nil { + return "", err + } + hostname, err := trustedOriginHostname(u) + if err != nil { + return "", err + } + u.Host = hostnameWithOptionalPort(hostname, u.Port()) + u.Scheme = "https" + u.Path = "" + u.RawPath = "" + return strings.TrimRight(u.String(), "/"), nil +} + +func parseTrustedHTTPSOrigin(raw string) (*url.URL, error) { + u, err := url.ParseRequestURI(strings.TrimSpace(raw)) + if err != nil { + return nil, fmt.Errorf("invalid trusted origin: %w", err) + } + if !strings.EqualFold(u.Scheme, "https") || u.Host == "" || u.Opaque != "" { + return nil, fmt.Errorf("invalid trusted origin: an absolute HTTPS origin is required") + } + if u.User != nil || u.RawQuery != "" || u.ForceQuery || u.Fragment != "" { + return nil, fmt.Errorf("invalid trusted origin: userinfo, query, and fragment are not allowed") + } + if u.EscapedPath() != "" && u.EscapedPath() != "/" { + return nil, fmt.Errorf("invalid trusted origin: a base origin without a path is required") + } + return u, nil +} + +func trustedOriginHostname(u *url.URL) (string, error) { + hostname := strings.ToLower(strings.TrimSuffix(u.Hostname(), ".")) + if hostname == "" || strings.ContainsAny(hostname, "* \t\r\n") { + return "", fmt.Errorf("invalid trusted origin: a concrete hostname is required") + } + if hostname == "localhost" || strings.HasSuffix(hostname, ".localhost") { + return "", fmt.Errorf("invalid trusted origin: local targets are not allowed") + } + if _, parseErr := netip.ParseAddr(hostname); parseErr == nil { + return "", fmt.Errorf("invalid trusted origin: IP-literal targets are not allowed") + } + if looksLikeNumericHost(hostname) { + return "", fmt.Errorf("invalid trusted origin: non-canonical numeric targets are not allowed") + } + if err := validateTrustedOriginPort(u.Port()); err != nil { + return "", err + } + return hostname, nil +} + +func validateTrustedOriginPort(port string) error { + if port != "" { + value, portErr := strconv.ParseUint(port, 10, 16) + if portErr != nil || value == 0 { + return fmt.Errorf("invalid trusted origin: port must be between 1 and 65535") + } + } + return nil +} + +func hostnameWithOptionalPort(hostname, port string) string { + if port != "" { + return hostname + ":" + port + } + return hostname +} + +func looksLikeNumericHost(hostname string) bool { + parts := strings.Split(hostname, ".") + if len(parts) == 0 { + return false + } + for _, part := range parts { + if part == "" { + continue + } + digits := part + base := byte(10) + if strings.HasPrefix(part, "0x") { + digits = part[2:] + base = 16 + } + if digits == "" { + return false + } + for _, r := range digits { + if r >= '0' && r <= '9' { + continue + } + if base == 16 && r >= 'a' && r <= 'f' { + continue + } + return false + } + } + return true +} diff --git a/internal/verifier/trusted_origin_test.go b/internal/verifier/trusted_origin_test.go new file mode 100644 index 0000000..5d7e4a3 --- /dev/null +++ b/internal/verifier/trusted_origin_test.go @@ -0,0 +1,42 @@ +package verifier + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNormalizeTrustedHTTPSOrigin(t *testing.T) { + tests := []struct { + name string + raw string + want string + }{ + {name: "canonical", raw: "https://provider.example", want: "https://provider.example"}, + {name: "trim and normalize host", raw: " https://PROVIDER.Example./ ", want: "https://provider.example"}, + {name: "self managed port", raw: "https://gitlab.example:8443/", want: "https://gitlab.example:8443"}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, err := NormalizeTrustedHTTPSOrigin(tc.raw) + require.NoError(t, err) + assert.Equal(t, tc.want, got) + }) + } + + for _, raw := range []string{ + "", "provider.example", "http://provider.example", "https://provider.example/path", + "https://provider.example?next=x", "https://provider.example/#fragment", + "https://user:pass@provider.example", "https://*.provider.example", + "https://localhost", "https://api.localhost", "https://127.0.0.1", + "https://127.1", "https://0x7f000001", "https://0x7f.0x0.0x0.0x1", + "https://provider.example:0", + "https://provider.example:65536", "https://[::1]", "https://[fe80::1%25en0]", + } { + t.Run("reject_"+raw, func(t *testing.T) { + _, err := NormalizeTrustedHTTPSOrigin(raw) + assert.Error(t, err) + }) + } +} diff --git a/internal/verifier/twilio/twilio_shared_test.go b/internal/verifier/twilio/twilio_shared_test.go new file mode 100644 index 0000000..bb7f130 --- /dev/null +++ b/internal/verifier/twilio/twilio_shared_test.go @@ -0,0 +1,20 @@ +package twilio + +import ( + "net/http" + "testing" + + "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/internal/verifier" + "github.com/HodeTech/leakwatch/internal/verifier/internal/vtest" +) + +func TestVerify_SharedSafetySuite(t *testing.T) { + vtest.Run(t, vtest.Case{Name: "twilio", New: func(url string, client *http.Client) verifier.Verifier { + return &Verifier{apiURL: url, httpClient: client} + }, Raw: detector.RawFinding{ + DetectorID: detectorID, + Raw: []byte("SyntheticTwilioAPIKeySecretFixture"), + ExtraData: map[string]string{"api_key_sid": "SKabcdef0123456789abcdef0123456789"}, + }}) +} diff --git a/internal/verifier/twilio/twilio_verifier.go b/internal/verifier/twilio/twilio_verifier.go index dea0820..0d35e79 100644 --- a/internal/verifier/twilio/twilio_verifier.go +++ b/internal/verifier/twilio/twilio_verifier.go @@ -1,11 +1,17 @@ -// Package twilio provides a verifier for Twilio API keys. -// It uses the Twilio Accounts API GET /2010-04-01/Accounts.json endpoint -// with Basic auth to check key validity. +// Package twilio provides conservative verification for paired Twilio API Key +// credentials. Production makes no request until a trusted regional API origin +// is supplied by operator-controlled configuration. package twilio import ( "context" + "encoding/json" + "errors" + "io" "net/http" + "net/url" + "regexp" + "strings" "github.com/HodeTech/leakwatch/internal/detector" "github.com/HodeTech/leakwatch/internal/verifier" @@ -15,80 +21,120 @@ import ( const detectorID = "twilio-api-key" -// defaultAPIURL is the base URL for the Twilio API. -const defaultAPIURL = "https://api.twilio.com" +var ( + apiKeySIDPattern = regexp.MustCompile(`^SK[0-9a-fA-F]{32}$`) + accountSIDPattern = regexp.MustCompile(`^AC[0-9a-fA-F]{32}$`) +) -// Verifier checks whether a Twilio API key is active by calling the -// Twilio Accounts API. It NEVER logs or persists raw key values. +// Verifier checks a Twilio API Key Secret with its paired API Key SID. apiURL +// is an unexported trusted-origin seam; the production registration leaves it +// empty because Twilio credentials are region-specific. type Verifier struct { - // apiURL overrides the Twilio API base URL (for testing). - apiURL string - // httpClient overrides the default HTTP client (for testing). + apiURL string httpClient *http.Client } +// NewForTrustedInstance accepts an operator-selected official Twilio regional +// API origin. Region selection is never inferred from scanned content. +func NewForTrustedInstance(instanceURL string) (*Verifier, error) { + normalized, err := verifier.NormalizeTrustedHTTPSOrigin(instanceURL) + if err != nil { + return nil, err + } + u, err := url.Parse(normalized) + hostname := u.Hostname() + if err != nil || u.Port() != "" || + (hostname != "api.twilio.com" && + (!strings.HasPrefix(hostname, "api.") || !strings.HasSuffix(hostname, ".twilio.com"))) { + return nil, errors.New("invalid Twilio API origin: an official twilio.com host is required") + } + return &Verifier{apiURL: normalized}, nil +} + +// WithTrustedInstance implements verifier.TrustedInstanceConfigurer. +func (*Verifier) WithTrustedInstance(instanceURL string) (verifier.Verifier, error) { + return NewForTrustedInstance(instanceURL) +} + func init() { verifier.Register(&Verifier{}) } // Type returns the detector ID this verifier handles. -func (v *Verifier) Type() string { - return detectorID -} +func (v *Verifier) Type() string { return detectorID } -// Verify checks if the detected Twilio API key is valid/active. -// -// The detector emits the API Key SID ("SK...") as raw.Raw and the Account SID -// ("AC...") as raw.ExtraData["account_sid"]. Twilio's REST API accepts exactly -// two Basic-Auth pairs: (Account SID, Auth Token) or (API Key SID, API Key -// Secret). Since raw.Raw is an API Key SID, the only valid pairing is -// (API Key SID, API Key Secret): the SID is the Basic-Auth username and the -// paired API Key Secret is the password. The API Key SID alone is a non-secret -// identifier and cannot authenticate on its own, so when the paired secret is -// not available (raw.ExtraData["api_key_secret"] is empty) the key is reported -// as Unverified — never as inactive, which would misread a live but unpaired -// credential as revoked. +// Verify authenticates with API Key SID as the Basic username and API Key +// Secret as the password. Main, Standard, and Restricted keys have different +// permissions: only 401 proves inactive; a permission-denied 403 is +// deliberately a verify_error. func (v *Verifier) Verify(ctx context.Context, raw detector.RawFinding) finding.VerificationResult { - apiKeySID := string(raw.Raw) - if apiKeySID == "" { - return finding.VerificationResult{ - Status: finding.StatusUnverified, - Message: "empty token", - } + apiKeySecret := string(raw.Raw) + if apiKeySecret == "" { + return finding.VerificationResult{Status: finding.StatusUnverified, Message: "empty token"} } - var accountSID, apiKeySecret string + apiKeySID := "" + accountSID := "" if raw.ExtraData != nil { + apiKeySID = raw.ExtraData["api_key_sid"] accountSID = raw.ExtraData["account_sid"] - apiKeySecret = raw.ExtraData["api_key_secret"] } - - if apiKeySecret == "" { - // Without the paired API Key Secret the credential cannot be - // authenticated. Report Unverified rather than guessing at liveness. + if !apiKeySIDPattern.MatchString(apiKeySID) { return finding.VerificationResult{ Status: finding.StatusUnverified, - Message: "API Key Secret unavailable; cannot verify", + Message: "paired Twilio API Key SID is required for verification", + } + } + if accountSID != "" && !accountSIDPattern.MatchString(accountSID) { + return finding.VerificationResult{ + Status: finding.StatusUnverified, + Message: "invalid Twilio Account SID context", + } + } + if v.apiURL == "" { + return finding.VerificationResult{ + Status: finding.StatusUnverified, + Message: "trusted Twilio regional API origin is not configured", } } - apiURL := httpx.BaseURL(v.apiURL, defaultAPIURL) - // When the Account SID is known, scope the request to that account; - // otherwise fall back to the accounts collection, which an API Key can also - // reach. path := "/2010-04-01/Accounts.json" if accountSID != "" { path = "/2010-04-01/Accounts/" + accountSID + ".json" } - return httpx.VerifyToken(ctx, v.httpClient, apiKeySID, httpx.TokenSpec{ + return httpx.VerifyToken(ctx, v.httpClient, apiKeySecret, httpx.TokenSpec{ Name: "twilio", Request: httpx.Request{ - URL: apiURL + path, + URL: v.apiURL + path, BasicAuthUser: apiKeySID, BasicAuthPass: apiKeySecret, }, - ActiveMessage: "Twilio API key is active", - InactiveMessage: "Twilio API key is invalid or revoked", + InactiveStatuses: []int{http.StatusUnauthorized}, + ActiveMessage: "Twilio API key is active for the trusted region and probe", + InactiveMessage: "Twilio API key is invalid or revoked on the trusted region", + Decode: func(body io.Reader) (map[string]string, string, error) { return decodeAccountProbe(body, accountSID) }, + RequireCompleteBody: true, + RequireJSONContentType: true, }) } + +func decodeAccountProbe(body io.Reader, expectedAccountSID string) (map[string]string, string, error) { + var response struct { + SID string `json:"sid"` + Accounts json.RawMessage `json:"accounts"` + } + if err := json.NewDecoder(body).Decode(&response); err != nil { + return nil, "", err + } + if expectedAccountSID != "" { + if response.SID != expectedAccountSID { + return nil, "", errors.New("twilio account probe returned an unexpected account") + } + return map[string]string{"account_sid": expectedAccountSID}, "", nil + } + if len(response.Accounts) == 0 || string(response.Accounts) == "null" { + return nil, "", errors.New("twilio account collection is missing") + } + return nil, "", nil +} diff --git a/internal/verifier/twilio/twilio_verifier_test.go b/internal/verifier/twilio/twilio_verifier_test.go index a0d29a2..7f67716 100644 --- a/internal/verifier/twilio/twilio_verifier_test.go +++ b/internal/verifier/twilio/twilio_verifier_test.go @@ -8,180 +8,172 @@ import ( "strings" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/HodeTech/leakwatch/internal/detector" + twiliodetector "github.com/HodeTech/leakwatch/internal/detector/twilio" "github.com/HodeTech/leakwatch/pkg/finding" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -// Fixtures mirror the real Twilio detector output: raw.Raw is the API Key SID -// ("SK" + 32 hex) and ExtraData["account_sid"] is the Account SID ("AC" + 32 -// hex). The API Key Secret is the paired secret required to authenticate. const ( fixtureAPIKeySID = "SKabcdef0123456789abcdef0123456789" - fixtureAccount = "AC1234567890abcdef1234567890abcd" - fixtureSecret = "api_key_secret_0123456789abcdef0" + fixtureAccount = "AC1234567890abcdef1234567890ABCDEF" ) -func TestVerify_ValidKey_ReturnsActive(t *testing.T) { +func fixtureAPIKeySecret() string { return strings.Repeat("Ab12Cd34", 4) } + +func detectorFixture(t *testing.T, includeAccount bool) detector.RawFinding { + t.Helper() + input := "TWILIO_API_KEY_SID=" + fixtureAPIKeySID + "\n" + if includeAccount { + input = "TWILIO_ACCOUNT_SID=" + fixtureAccount + "\n" + input + } + input += "TWILIO_API_KEY_" + "SECRET=" + fixtureAPIKeySecret() + findings := (&twiliodetector.Detector{}).Scan(context.Background(), []byte(input)) + require.Len(t, findings, 1) + return findings[0] +} + +func TestVerify_RealDetectorFinding_ReachesTrustedProbe(t *testing.T) { + secret := fixtureAPIKeySecret() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "/2010-04-01/Accounts/"+fixtureAccount+".json", r.URL.Path) - - // Verify Basic auth: API Key SID as username, API Key Secret as password. auth := r.Header.Get("Authorization") require.True(t, strings.HasPrefix(auth, "Basic ")) decoded, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(auth, "Basic ")) require.NoError(t, err) - parts := strings.SplitN(string(decoded), ":", 2) - assert.Equal(t, fixtureAPIKeySID, parts[0]) - assert.Equal(t, fixtureSecret, parts[1]) + assert.Equal(t, fixtureAPIKeySID+":"+secret, string(decoded)) w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"sid":"` + fixtureAccount + `"}`)) })) defer server.Close() - v := &Verifier{ - apiURL: server.URL, - httpClient: server.Client(), - } - - raw := detector.RawFinding{ - DetectorID: detectorID, - Raw: []byte(fixtureAPIKeySID), - Redacted: "SK****6789", - ExtraData: map[string]string{ - "account_sid": fixtureAccount, - "api_key_secret": fixtureSecret, - }, - } - - result := v.Verify(context.Background(), raw) + result := (&Verifier{apiURL: server.URL, httpClient: server.Client()}).Verify( + context.Background(), detectorFixture(t, true), + ) require.Equal(t, finding.StatusVerifiedActive, result.Status) - assert.Equal(t, "Twilio API key is active", result.Message) -} - -func TestVerify_InvalidKey_ReturnsInactive(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusUnauthorized) - _, _ = w.Write([]byte(`{"message":"Authenticate"}`)) - })) - defer server.Close() - - v := &Verifier{ - apiURL: server.URL, - httpClient: server.Client(), - } - - raw := detector.RawFinding{ - DetectorID: detectorID, - Raw: []byte(fixtureAPIKeySID), - Redacted: "SK****6789", - ExtraData: map[string]string{ - "account_sid": fixtureAccount, - "api_key_secret": fixtureSecret, - }, + assert.Equal(t, fixtureAccount, result.ExtraData["account_sid"]) + assert.NotContains(t, result.Message, secret) + for _, value := range result.ExtraData { + assert.NotEqual(t, secret, value) } +} - result := v.Verify(context.Background(), raw) +func TestVerify_ProductionFindingWithoutTrustedOrigin_IsUnverified(t *testing.T) { + result := (&Verifier{}).Verify(context.Background(), detectorFixture(t, true)) - assert.Equal(t, finding.StatusVerifiedInactive, result.Status) - assert.Equal(t, "Twilio API key is invalid or revoked", result.Message) + assert.Equal(t, finding.StatusUnverified, result.Status) + assert.Equal(t, "trusted Twilio regional API origin is not configured", result.Message) } -func TestVerify_ServerError_ReturnsError(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - })) - defer server.Close() - - v := &Verifier{ - apiURL: server.URL, - httpClient: server.Client(), +func TestNewForTrustedInstance_AcceptsOnlyTwilioAPIOrigins(t *testing.T) { + for _, origin := range []string{ + "https://api.twilio.com", "https://api.ie1.twilio.com", "https://api.sydney.au1.twilio.com", + } { + configured, err := NewForTrustedInstance(origin) + require.NoError(t, err, origin) + assert.Equal(t, origin, configured.apiURL) } - - raw := detector.RawFinding{ - DetectorID: detectorID, - Raw: []byte(fixtureAPIKeySID), - Redacted: "SK****6789", - ExtraData: map[string]string{ - "account_sid": fixtureAccount, - "api_key_secret": fixtureSecret, - }, + for _, origin := range []string{ + "https://twilio.com", "https://events.twilio.com", "https://api.twilio.com.attacker.example", + "https://api.twilio.com:443", "http://api.twilio.com", + } { + _, err := NewForTrustedInstance(origin) + assert.Error(t, err, origin) } - - result := v.Verify(context.Background(), raw) - - assert.Equal(t, finding.StatusVerifyError, result.Status) - assert.Contains(t, result.Message, "500") } -// TestVerify_NoAPIKeySecret_ReturnsUnverified verifies that the common real -// detector output (API Key SID + Account SID, but no paired secret) is reported -// as Unverified — never falsely inactive. -func TestVerify_NoAPIKeySecret_ReturnsUnverified(t *testing.T) { - v := &Verifier{} - - raw := detector.RawFinding{ - DetectorID: detectorID, - Raw: []byte(fixtureAPIKeySID), - Redacted: "SK****6789", - ExtraData: map[string]string{"account_sid": fixtureAccount}, +func TestVerify_Only401IsInactive_403IsPermissionError(t *testing.T) { + tests := []struct { + name string + status int + want finding.VerificationStatus + }{ + {name: "authentication rejection", status: http.StatusUnauthorized, want: finding.StatusVerifiedInactive}, + {name: "permission rejection", status: http.StatusForbidden, want: finding.StatusVerifyError}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(tc.status) + })) + defer server.Close() + + result := (&Verifier{apiURL: server.URL, httpClient: server.Client()}).Verify( + context.Background(), detectorFixture(t, true), + ) + assert.Equal(t, tc.want, result.Status) + }) } - - result := v.Verify(context.Background(), raw) - - assert.Equal(t, finding.StatusUnverified, result.Status) - assert.Equal(t, "API Key Secret unavailable; cannot verify", result.Message) } -// TestVerify_NoAccountSID_UsesCollectionEndpoint verifies that the request -// falls back to the accounts collection when no Account SID is known. -func TestVerify_NoAccountSID_UsesCollectionEndpoint(t *testing.T) { +func TestVerify_NoAccountSID_UsesCollectionProbe(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "/2010-04-01/Accounts.json", r.URL.Path) w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"accounts":[]}`)) })) defer server.Close() - v := &Verifier{ - apiURL: server.URL, - httpClient: server.Client(), - } - - raw := detector.RawFinding{ - DetectorID: detectorID, - Raw: []byte(fixtureAPIKeySID), - Redacted: "SK****6789", - ExtraData: map[string]string{"api_key_secret": fixtureSecret}, - } - - result := v.Verify(context.Background(), raw) - + result := (&Verifier{apiURL: server.URL, httpClient: server.Client()}).Verify( + context.Background(), detectorFixture(t, false), + ) assert.Equal(t, finding.StatusVerifiedActive, result.Status) } -func TestVerify_Type_ReturnsCorrectID(t *testing.T) { - v := &Verifier{} - assert.Equal(t, "twilio-api-key", v.Type()) +func TestVerify_MissingOrInvalidCompanionContext_IsUnverified(t *testing.T) { + secret := fixtureAPIKeySecret() + tests := []detector.RawFinding{ + {Raw: []byte(secret)}, + {Raw: []byte(secret), ExtraData: map[string]string{"api_key_sid": "not-a-sid"}}, + {Raw: []byte(secret), ExtraData: map[string]string{"api_key_sid": fixtureAPIKeySID, "account_sid": "not-an-account"}}, + } + for _, raw := range tests { + result := (&Verifier{apiURL: "http://127.0.0.1:1"}).Verify(context.Background(), raw) + assert.Equal(t, finding.StatusUnverified, result.Status) + } } -func TestVerify_EmptyToken_ReturnsUnverified(t *testing.T) { - v := &Verifier{} - - raw := detector.RawFinding{ - DetectorID: detectorID, - Raw: []byte(""), - Redacted: "", +func TestVerify_MalformedSuccessResponse_IsVerifyError(t *testing.T) { + tests := []struct { + contentType string + body string + }{ + {contentType: "application/json", body: `{}`}, + {contentType: "text/html", body: `{"sid":"` + fixtureAccount + `"}`}, + {contentType: "application/json", body: `{"sid":"ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}`}, } + for _, tc := range tests { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", tc.contentType) + _, _ = w.Write([]byte(tc.body)) + })) + result := (&Verifier{apiURL: server.URL, httpClient: server.Client()}).Verify( + context.Background(), detectorFixture(t, true), + ) + server.Close() + assert.Equal(t, finding.StatusVerifyError, result.Status) + } +} + +func TestVerify_ServerError_ReturnsError(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + })) + defer server.Close() - result := v.Verify(context.Background(), raw) + result := (&Verifier{apiURL: server.URL, httpClient: server.Client()}).Verify( + context.Background(), detectorFixture(t, true), + ) + assert.Equal(t, finding.StatusVerifyError, result.Status) +} +func TestVerify_TypeAndEmptyToken(t *testing.T) { + assert.Equal(t, detectorID, (&Verifier{}).Type()) + result := (&Verifier{}).Verify(context.Background(), detector.RawFinding{}) assert.Equal(t, finding.StatusUnverified, result.Status) assert.Equal(t, "empty token", result.Message) } diff --git a/internal/verifier/vercel/vercel_shared_test.go b/internal/verifier/vercel/vercel_shared_test.go new file mode 100644 index 0000000..cc54fee --- /dev/null +++ b/internal/verifier/vercel/vercel_shared_test.go @@ -0,0 +1,16 @@ +package vercel + +import ( + "net/http" + "testing" + + "github.com/HodeTech/leakwatch/internal/detector" + "github.com/HodeTech/leakwatch/internal/verifier" + "github.com/HodeTech/leakwatch/internal/verifier/internal/vtest" +) + +func TestVerify_SharedSafetySuite(t *testing.T) { + vtest.Run(t, vtest.Case{Name: "vercel", New: func(url string, client *http.Client) verifier.Verifier { + return &Verifier{apiURL: url, httpClient: client} + }, Raw: detector.RawFinding{DetectorID: detectorID, Raw: []byte("vercel_SyntheticVercelCredentialFixture123")}}) +} diff --git a/internal/verifier/verifier.go b/internal/verifier/verifier.go index 4160f2b..e1daece 100644 --- a/internal/verifier/verifier.go +++ b/internal/verifier/verifier.go @@ -23,6 +23,33 @@ type Verifier interface { Verify(ctx context.Context, raw detector.RawFinding) finding.VerificationResult } +// RequestGate is an engine-owned admission gate for a single outbound +// verification request. A RequestGatedVerifier must call it immediately before +// every provider request and must return a non-nil result without sending the +// request when admission fails. +type RequestGate func() *finding.VerificationResult + +// RequestGatedVerifier is implemented by verifiers that need explicit request +// admission because they use an SDK/hand-rolled client or may issue more than +// one provider request (for example a bounded regional fallback). The engine +// uses the declared maximum to fail closed on runaway attempts while pacing +// each request at its actual send point. +type RequestGatedVerifier interface { + Verifier + + // VerificationRequestBudget is the maximum number of provider requests that + // VerifyWithRequestGate may attempt for one finding. + VerificationRequestBudget() int + + // VerifyWithRequestGate performs verification and calls gate immediately + // before every outbound provider request. + VerifyWithRequestGate( + ctx context.Context, + raw detector.RawFinding, + gate RequestGate, + ) finding.VerificationResult +} + // VerifyPair associates a Finding with its corresponding RawFinding // so the verification engine can pass raw secret data to verifiers // while returning enriched Finding objects. diff --git a/pkg/finding/finding.go b/pkg/finding/finding.go index 1e3a89b..c38b1a7 100644 --- a/pkg/finding/finding.go +++ b/pkg/finding/finding.go @@ -226,15 +226,72 @@ type Finding struct { SourceMetadata SourceMetadata `json:"source"` Verification VerificationResult `json:"verification"` Remediation *Remediation `json:"remediation,omitempty"` - DetectedAt time.Time `json:"detected_at"` - // Entropy is the Shannon entropy of the raw match, populated only when - // entropy analysis is enabled. It is omitted (via omitempty) when it holds - // the Go zero value, so a serialized finding cannot distinguish "not - // computed" from "computed as exactly 0.0"; consumers should treat an - // absent entropy as "not computed" rather than zero. - Entropy float64 `json:"entropy,omitempty"` + DetectedAt time.Time `json:"detected_at,omitempty"` + // Entropy is the Shannon entropy of the raw match. EntropyCalculated records + // whether the value was actually computed, so a legitimate 0.0 is distinct + // from "not computed" on the JSON wire without changing the long-standing + // public Entropy float64 field. Call SetEntropy when constructing findings + // outside the engine. For source compatibility, a non-zero Entropy value is + // also serialized even when an older caller did not set EntropyCalculated. + Entropy float64 `json:"entropy,omitempty"` + EntropyCalculated bool `json:"-"` // ExtraData carries non-secret contextual metadata. It is json:"-" as a // defense-in-depth measure (see the type doc); it must never hold secret // material. ExtraData map[string]string `json:"-"` } + +// SetEntropy records a computed Shannon entropy value, including a legitimate +// zero. The explicit presence bit preserves the public float64 API while making +// the JSON representation nullable. +func (f *Finding) SetEntropy(value float64) { + f.Entropy = value + f.EntropyCalculated = true +} + +// MarshalJSON omits zero DetectedAt and emits entropy only when it was +// calculated. A non-zero legacy value remains visible for source compatibility +// with callers written before EntropyCalculated existed. +func (f Finding) MarshalJSON() ([]byte, error) { + type alias Finding + shadow := struct { + alias + DetectedAt *time.Time `json:"detected_at,omitempty"` + Entropy *float64 `json:"entropy,omitempty"` + }{alias: alias(f)} + if !f.DetectedAt.IsZero() { + shadow.DetectedAt = &f.DetectedAt + } + if f.EntropyCalculated || f.Entropy != 0 { + shadow.Entropy = &f.Entropy + } + out, err := json.Marshal(shadow) + if err != nil { + return nil, fmt.Errorf("failed to marshal Finding JSON: %w", err) + } + return out, nil +} + +// UnmarshalJSON restores the entropy presence bit so a computed zero survives a +// JSON round trip. Missing timestamps and entropy values retain their Go zero +// values. +func (f *Finding) UnmarshalJSON(data []byte) error { + type alias Finding + var decoded alias + shadow := struct { + *alias + DetectedAt *time.Time `json:"detected_at,omitempty"` + Entropy *float64 `json:"entropy,omitempty"` + }{alias: &decoded} + if err := json.Unmarshal(data, &shadow); err != nil { + return fmt.Errorf("failed to unmarshal Finding JSON: %w", err) + } + *f = Finding(decoded) + if shadow.DetectedAt != nil { + f.DetectedAt = *shadow.DetectedAt + } + if shadow.Entropy != nil { + f.SetEntropy(*shadow.Entropy) + } + return nil +} diff --git a/pkg/finding/finding_test.go b/pkg/finding/finding_test.go index d96acad..b02ddd0 100644 --- a/pkg/finding/finding_test.go +++ b/pkg/finding/finding_test.go @@ -215,8 +215,8 @@ func TestFinding_JSONMarshalUnmarshal_SeverityAsString(t *testing.T) { Status: StatusUnverified, }, DetectedAt: now, - Entropy: 4.5, } + f.SetEntropy(4.5) data, err := json.Marshal(f) require.NoError(t, err) @@ -243,9 +243,58 @@ func TestFinding_JSONMarshalUnmarshal_SeverityAsString(t *testing.T) { assert.Equal(t, f.SourceMetadata.FilePath, decoded.SourceMetadata.FilePath) assert.Equal(t, f.SourceMetadata.Line, decoded.SourceMetadata.Line) assert.Equal(t, f.Verification.Status, decoded.Verification.Status) + assert.True(t, decoded.EntropyCalculated) + assert.Equal(t, 4.5, decoded.Entropy) assert.Empty(t, decoded.Raw) // Raw is never serialized via json:"-" } +func TestFinding_JSONNullableEntropyAndDetectedAt(t *testing.T) { + t.Run("missing values are omitted", func(t *testing.T) { + data, err := json.Marshal(Finding{ID: "test-1"}) + require.NoError(t, err) + + var object map[string]any + require.NoError(t, json.Unmarshal(data, &object)) + assert.NotContains(t, object, "detected_at") + assert.NotContains(t, object, "entropy") + assert.NotContains(t, string(data), "0001-01-01") + }) + + t.Run("computed zero is present and round trips", func(t *testing.T) { + original := Finding{ID: "test-2"} + original.SetEntropy(0) + + data, err := json.Marshal(original) + require.NoError(t, err) + var object map[string]any + require.NoError(t, json.Unmarshal(data, &object)) + value, exists := object["entropy"] + require.True(t, exists) + assert.Equal(t, float64(0), value) + + var decoded Finding + require.NoError(t, json.Unmarshal(data, &decoded)) + assert.True(t, decoded.EntropyCalculated) + assert.Zero(t, decoded.Entropy) + }) + + t.Run("legacy non-zero entropy remains source compatible", func(t *testing.T) { + data, err := json.Marshal(Finding{ID: "test-3", Entropy: 3.5}) + require.NoError(t, err) + assert.Contains(t, string(data), `"entropy":3.5`) + }) + + t.Run("missing values clear a reused destination", func(t *testing.T) { + destination := Finding{DetectedAt: time.Now().UTC()} + destination.SetEntropy(4.2) + require.NoError(t, json.Unmarshal([]byte(`{"id":"replacement"}`), &destination)) + assert.Equal(t, "replacement", destination.ID) + assert.True(t, destination.DetectedAt.IsZero()) + assert.Zero(t, destination.Entropy) + assert.False(t, destination.EntropyCalculated) + }) +} + func TestFinding_JSONOmitsEmptyRaw(t *testing.T) { f := Finding{ ID: "test-1", diff --git a/site/README.md b/site/README.md index f5d6b88..045ae31 100644 --- a/site/README.md +++ b/site/README.md @@ -72,16 +72,21 @@ POST without JavaScript. It includes a honeypot field and never asks for secrets ## Editing the documentation -Manual pages are authored as Markdown under +Current product behavior is authored in the English Markdown manual under [`../docs/user-manuals/`](../docs/user-manuals/): ``` docs/user-manuals/ ├── _meta.yaml navigation: sections, page order, titles -├── en/
/.md English source -└── tr/
/.md Turkish source +├── en/
/.md authoritative English source +└── tr/
/.md reviewed Turkish translation ``` +Update the English page first, then its Turkish counterpart in the same change. +Files under `js/manuals/` are generated publication artifacts and must never be +edited by hand. Supplemental long-form guides under `docs/guides/` may add +operational depth, but they defer product contracts to the user manual. + Each page has YAML front matter (`title`, `description`), GFM Markdown, fenced code blocks, optional `:::tip` / `:::note` / `:::warn` / `:::danger` callouts, and ` ```mermaid ` diagrams. Cross-links use the hash-router form diff --git a/site/assets/og.svg b/site/assets/og.svg index 95593f7..48d9931 100644 --- a/site/assets/og.svg +++ b/site/assets/og.svg @@ -21,10 +21,10 @@ - 64 detectors - 54 live verifiers - 6 sources - SARIF · JSON · CSV + 65 detectors + 39 direct-live checks + 6 sources + SARIF · JSON · CSV github.com/HodeTech/Leakwatch · MIT diff --git a/site/contact.html b/site/contact.html index 3490a57..dd547f6 100644 --- a/site/contact.html +++ b/site/contact.html @@ -141,7 +141,7 @@

License (MIT)

- + diff --git a/site/css/style.css b/site/css/style.css index 64d3b85..5374ec1 100644 --- a/site/css/style.css +++ b/site/css/style.css @@ -199,7 +199,7 @@ h1,h2,h3,h4{font-family:var(--display);font-weight:700;letter-spacing:-.015em;co .vrow{display:flex;align-items:center;gap:14px;padding:14px 0;border-bottom:1px solid var(--line)} .vrow:last-child{border-bottom:0} .vrow .vk{font-family:var(--mono);font-size:12px;font-weight:700;min-width:130px} -.vrow .vk.live{color:var(--cleared)}.vrow .vk.fmt{color:var(--info)}.vrow .vk.no{color:var(--faint)} +.vrow .vk.live{color:var(--cleared)}.vrow .vk.ctx{color:var(--warn)}.vrow .vk.fmt{color:var(--info)}.vrow .vk.no{color:var(--faint)} .vrow .vd{color:var(--muted);font-size:13.5px} .vcount{font-family:var(--display);font-weight:700;color:var(--paper);margin-left:auto} diff --git a/site/docs.html b/site/docs.html index 3d6f5d2..acb6b7d 100644 --- a/site/docs.html +++ b/site/docs.html @@ -110,7 +110,7 @@
  • License (MIT)
  • - + diff --git a/site/index.html b/site/index.html index b92173f..18dae92 100644 --- a/site/index.html +++ b/site/index.html @@ -10,11 +10,11 @@ - + - + @@ -68,7 +68,7 @@

    Some secrets shouldn't be ▸ Run the scan Read the docs -
    64 detectors · 54 live verifiers · 6 sources · exit-code aware
    +
    65 detectors · 39 direct-live · 9 context-required · 6 sources
    @@ -95,10 +95,10 @@

    Some secrets shouldn't be
    -
    64
    secret detectors
    -
    54
    live verifiers
    +
    65
    secret detectors
    +
    39
    direct live checks
    6
    scan sources
    -
    4
    output formats
    +
    5
    output formats
    @@ -144,31 +144,33 @@

    Some secrets shouldn't be
    § 04

    Is it still live?

    -

    Detection is half the job. For most secret types, Leakwatch makes a controlled, read-only API call to the provider to confirm whether a key is active — separating real incidents from noise.

    +

    Detection is half the job. Leakwatch distinguishes direct live checks from checks that require trusted context and from offline format validation, so registry counts never overstate live capability.

    -
    Live verifiedA read-only API call confirms the key is active or inactive.~49
    -
    Format checkedValidated by structure where no safe live check exists.5
    -
    Not verifiableNo public verification API (e.g. JWTs, private keys) — still detected, triaged manually.9
    +
    Direct liveA non-destructive provider check can confirm the key in the normal production path.39
    +
    Context requiredA trusted issuer, region, or paired credential must be supplied before a safe live check.9
    +
    Format checkedValidated by structure where no safe live check exists.6
    +
    Not verifiableNo safe provider check (e.g. JWTs, private keys) — still detected, triaged manually.11
    § 05

    Reports that fit your workflow

    -
    +
    JSON

    Structured findings for tooling and automation.

    SARIF

    v2.1.0 — upload straight to GitHub Code Scanning.

    CSV

    Spreadsheet-ready, sanitized against formula injection.

    Table

    Colorized terminal output for local triage.

    +
    GitHub

    Inline pull-request annotations in GitHub Actions.

    -
    § 06

    Redacted index — 64 detectors

    -

    A sample of the catalog. 54 of these can be verified against the live provider. Add your own with YAML custom rules.

    +
    § 06

    Redacted index — 65 detectors

    +

    A sample of the catalog: 39 direct-live, 9 context-required, 6 format-only, and 11 without a verifier. Add your own with YAML custom rules.

    - aws-access-key-idgcp-service-accountopenai-api-keyanthropic-api-keygithub-tokenstripe-api-key-livesendgrid-api-keyslack-tokentwilio-api-keydigitalocean-tokendatadog-api-keycloudflare-api-tokennpm-tokenprivate-keyjwtshopify-access-tokensupabase-service-keygitlab-patvercel-token+ 44 more · view full catalog → + aws-access-key-idgcp-service-accountopenai-api-keyanthropic-api-keygithub-tokenstripe-api-key-livesendgrid-api-keyslack-tokentwilio-api-keydigitalocean-tokendatadog-api-keycloudflare-api-tokennpm-tokenprivate-keyjwtshopify-access-tokensupabase-service-keygitlab-patvercel-token+ 46 more · view full catalog →
    @@ -214,7 +216,7 @@

    Declassify your repo

  • License (MIT)
  • - + diff --git a/site/js/detectors.js b/site/js/detectors.js index 9d89f64..67a3a7b 100644 --- a/site/js/detectors.js +++ b/site/js/detectors.js @@ -37,7 +37,7 @@ window.LW_DETECTORS = [ ], "patterns": [ { - "src": "(?:AUTH0_MANAGEMENT_TOKEN|AUTH0_API_TOKEN|auth0_token)\\s*[=:]\\s*['\"]?([A-Za-z0-9_-]{30,})['\"]?" + "src": "(?:[\"']?(?:AUTH0_MANAGEMENT_TOKEN|AUTH0_API_TOKEN|auth0_token)[\"']?)[ \\t]*[=:][ \\t]*[\"']?([A-Za-z0-9_-]{16,}\\.[A-Za-z0-9_-]{16,}\\.[A-Za-z0-9_-]{8,})" } ] }, @@ -344,17 +344,20 @@ window.LW_DETECTORS = [ "glpat-", "gldt-", "glrt-", + "glrtr-", "glcbt-", "glptt-", + "glimt-", + "glagent-", + "glwt-", + "glsoat-", + "glffct-", "gloas-", "glft-" ], "patterns": [ { - "src": "(?:glpat|gldt|glrt|glcbt|glptt|gloas|glft)-[A-Za-z0-9_\\-]{20,}" - }, - { - "src": "https?://([a-zA-Z0-9.-]*gitlab[a-zA-Z0-9.-]*(?::\\d+)?)" + "src": "(?:glpat|gldt|glrt|glrtr|glcbt|glptt|glimt|glagent|glwt|glsoat|glffct|gloas|glft)-[A-Za-z0-9_\\-]{20,}" } ] }, @@ -690,7 +693,7 @@ window.LW_DETECTORS = [ ], "patterns": [ { - "src": "shpat_[a-f0-9]{32}" + "src": "\\bshpat_[a-f0-9]{32}\\b" } ] }, @@ -793,7 +796,7 @@ window.LW_DETECTORS = [ ], "patterns": [ { - "src": "sbp_[a-f0-9]{40}" + "src": "\\bsbp_[a-f0-9]{40}\\b" } ] }, @@ -837,15 +840,27 @@ window.LW_DETECTORS = [ { "id": "twilio-api-key", "severity": "critical", - "keywords": [], + "keywords": [ + "secret" + ], "patterns": [ { - "src": "SK[a-f0-9]{32}" - }, - { - "src": "AC[a-f0-9]{32}" + "src": "(?:^|[^0-9A-Za-z_])(?:(?:[a-z0-9]+[._-]+)*twilio[._-]*|x[._-]*)?api[._-]*(?:key[._-]*)?secret[\"']?\\s*[:=]\\s*(?:\"([^\"\\r\\n]{1,512})\"|'([^'\\r\\n]{1,512})'|([^\"' \\t\\r\\n,;}#]{1,512})(?:[ \\t\\r\\n,;}#]|$))", + "flags": "i" } - ] + ], + "correlation": { + "requiredNearby": [ + { + "src": "(?:^|[^0-9A-Za-z_])(?:(?:[a-z0-9]+[._-]+)*twilio[._-]*|x[._-]*)?api[._-]*(?:key[._-]*)?sid[\"']?\\s*[:=]\\s*[\"']?(SK[0-9a-f]{32})(?:[\"']|[ \\t\\r\\n,;}#]|$)", + "flags": "i" + } + ], + "proximityBytes": 512, + "sameLogicalBlock": true, + "rejectPlaceholders": true, + "oneToOne": true + } }, { "id": "vercel-token", diff --git a/site/js/manuals/en.js b/site/js/manuals/en.js index 4ba657a..0fcd6c3 100644 --- a/site/js/manuals/en.js +++ b/site/js/manuals/en.js @@ -1,3 +1,3 @@ // Generated by tools/site-build. Do not edit by hand. window.LW_MANUAL = window.LW_MANUAL || {}; -window.LW_MANUAL["en"] = {"ci-cd/docker-usage":{"title":"Docker Usage","description":"Run Leakwatch scans inside a container using the official Docker image.","html":"\u003ch1 id=\"docker-usage\"\u003eDocker Usage\u003c/h1\u003e\n\u003cp\u003eThe official Leakwatch container image lets you run scans without installing anything on the host machine. Because the image is statically compiled with \u003ccode\u003eCGO_ENABLED=0\u003c/code\u003e and runs as a non-root user, it is safe to use in locked-down CI environments and on shared machines where you do not want to modify the host system.\u003c/p\u003e\n\u003ch2 id=\"image-reference\"\u003eImage reference\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eghcr.io/hodetech/leakwatch\n\u003c/code\u003e\u003c/pre\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eTag\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e:latest\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMost recent release\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e:v1.5.0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eExact version pin\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e:v1.5\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMinor-version pin (tracks patch releases)\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eThe image is based on Alpine, runs as the non-root user \u003ccode\u003eleakwatch\u003c/code\u003e, uses \u003ccode\u003e/scan\u003c/code\u003e as the working directory, and has \u003ccode\u003eleakwatch\u003c/code\u003e as its entrypoint.\u003c/p\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNote\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eBecause the entrypoint is \u003ccode\u003eleakwatch\u003c/code\u003e, you append the subcommand and flags directly after the image name — for example, \u003ccode\u003eghcr.io/hodetech/leakwatch:latest scan fs /scan\u003c/code\u003e. There is no need to repeat the binary name.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"scanning-a-local-directory\"\u003eScanning a local directory\u003c/h2\u003e\n\u003cp\u003eMount the directory you want to scan to \u003ccode\u003e/scan\u003c/code\u003e inside the container:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003edocker run --rm \\\n -v \u0026quot;$(pwd):/scan\u0026quot; \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan fs /scan\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eTo write results to a file on the host, write the output file into the mounted volume:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003edocker run --rm \\\n -v \u0026quot;$(pwd):/scan\u0026quot; \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan fs /scan --format sarif -o /scan/leakwatch.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThe file \u003ccode\u003eleakwatch.sarif\u003c/code\u003e appears in the current directory on your host after the container exits.\u003c/p\u003e\n\u003ch2 id=\"scanning-a-remote-git-repository\"\u003eScanning a remote Git repository\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003edocker run --rm \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan git https://github.com/org/repo.git --format json\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eNo volume mount is required for remote Git repositories — Leakwatch clones them into a temporary directory inside the container.\u003c/p\u003e\n\u003ch2 id=\"scanning-a-container-image\"\u003eScanning a container image\u003c/h2\u003e\n\u003cp\u003eLeakwatch is daemonless: it pulls image layers directly from the registry without a Docker daemon. This means you can scan a remote image from within the Leakwatch container without mounting the host Docker socket:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003edocker run --rm \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan image registry.example.com/my-app:v2.3.0\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eFor private registries, pass the credentials as environment variables consumed by the registry client (for example, \u003ccode\u003eDOCKER_CONFIG\u003c/code\u003e pointing to a mounted credentials file, or the standard registry environment variables your registry supports).\u003c/p\u003e\n\u003ch2 id=\"passing-a-configuration-file\"\u003ePassing a configuration file\u003c/h2\u003e\n\u003cp\u003eMount \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e into \u003ccode\u003e/scan\u003c/code\u003e so Leakwatch picks it up automatically:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003edocker run --rm \\\n -v \u0026quot;$(pwd):/scan\u0026quot; \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan fs /scan\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eAs long as \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e is in the mounted directory, Leakwatch finds it because \u003ccode\u003e/scan\u003c/code\u003e is both the working directory and the path passed to the scan. If your config file lives elsewhere, mount it explicitly and use \u003ccode\u003e--config\u003c/code\u003e:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003edocker run --rm \\\n -v \u0026quot;$(pwd):/scan\u0026quot; \\\n -v \u0026quot;/path/to/custom-config.yaml:/config/leakwatch.yaml:ro\u0026quot; \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan fs /scan --config /config/leakwatch.yaml\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"passing-environment-variables\"\u003ePassing environment variables\u003c/h2\u003e\n\u003cp\u003eEnvironment variables for cloud scanning and token-based authentication can be injected with \u003ccode\u003e-e\u003c/code\u003e:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# S3 scan with AWS credentials\ndocker run --rm \\\n -e AWS_ACCESS_KEY_ID=AKIA••••••••••••EXAMPLE \\\n -e AWS_SECRET_ACCESS_KEY=••••••••••••••••••••••••••••••••••••••• \\\n -e AWS_REGION=us-east-1 \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan s3 my-bucket\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eFor CI environments, prefer injecting secrets as masked CI variables rather than embedding them in the command line.\u003c/p\u003e\n\u003ch2 id=\"output-file-pattern\"\u003eOutput file pattern\u003c/h2\u003e\n\u003cp\u003eA common Docker pattern in CI is to write results into the mounted volume and then upload or archive the file as a pipeline artifact:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003edocker run --rm \\\n -v \u0026quot;$(pwd):/scan\u0026quot; \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan fs /scan \\\n --format json \\\n --only-verified \\\n -o /scan/leakwatch-results.json\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/installation\"\u003eInstallation\u003c/a\u003e — install the native binary instead of using Docker.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/filesystem\"\u003eFilesystem Scanning\u003c/a\u003e — \u003ccode\u003escan fs\u003c/code\u003e flags and behavior.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/container-images\"\u003eContainer Images\u003c/a\u003e — scanning OCI/Docker image layers for secrets.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/other-ci\"\u003eOther CI Systems\u003c/a\u003e — using the Docker image in GitLab CI and other pipelines.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Reference\u003c/a\u003e — complete flag reference for all subcommands.\u003c/li\u003e\n\u003c/ul\u003e\n"},"ci-cd/github-action":{"title":"GitHub Action","description":"Use the official Leakwatch GitHub Action to scan for secrets in your GitHub workflows.","html":"\u003ch1 id=\"github-action\"\u003eGitHub Action\u003c/h1\u003e\n\u003cp\u003eEvery push to your repository is an opportunity for a secret to slip through. The official \u003cstrong\u003eLeakwatch GitHub Action\u003c/strong\u003e — published on the GitHub Marketplace and used as \u003ccode\u003eHodeTech/Leakwatch@v1\u003c/code\u003e — integrates Leakwatch directly into your GitHub workflow. It downloads the prebuilt Leakwatch binary for the runner (no Go toolchain or compilation step), runs a scan, maps exit codes, writes a job summary, and optionally uploads SARIF results to GitHub Code Scanning — all without any external service dependency.\u003c/p\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNote\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003e\u003cstrong\u003eSupported runners:\u003c/strong\u003e the action runs on Linux (\u003ccode\u003eubuntu-*\u003c/code\u003e) and macOS (\u003ccode\u003emacos-*\u003c/code\u003e) runners. Windows runners are not supported yet; run the scan on a Linux/macOS runner or use the container image \u003ccode\u003eghcr.io/hodetech/leakwatch\u003c/code\u003e.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"quick-start\"\u003eQuick start\u003c/h2\u003e\n\u003cp\u003eThe minimal configuration blocks the workflow when secrets are found:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e# .github/workflows/leakwatch-minimal.yml\nname: Secret scan (minimal)\n\non: [push, pull_request]\n\njobs:\n leakwatch:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: HodeTech/Leakwatch@v1\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eWith only the defaults, the action scans the filesystem (\u003ccode\u003escan-type: fs\u003c/code\u003e), produces SARIF output, skips live verification (\u003ccode\u003eno-verify: true\u003c/code\u003e), and fails the job if any finding is reported.\u003c/p\u003e\n\u003ch2 id=\"full-example-with-sarif-upload\"\u003eFull example with SARIF upload\u003c/h2\u003e\n\u003cp\u003eThe following workflow enables SARIF upload to GitHub Code Scanning, which surfaces findings as security alerts inside the repository:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e# .github/workflows/leakwatch.yml\nname: Secret scan\n\non:\n push:\n branches: [\u0026quot;main\u0026quot;, \u0026quot;develop\u0026quot;]\n pull_request:\n\npermissions:\n contents: read\n security-events: write # required for SARIF upload\n\njobs:\n leakwatch:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n\n - name: Scan for secrets\n uses: HodeTech/Leakwatch@v1\n with:\n scan-type: fs\n path: .\n format: sarif\n no-verify: \u0026quot;true\u0026quot;\n min-severity: low\n sarif-upload: \u0026quot;true\u0026quot;\n fail-on-findings: \u0026quot;true\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNote\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eSARIF upload requires the job to declare \u003ccode\u003epermissions: security-events: write\u003c/code\u003e. Without it, the upload step fails with a 403 error. The \u003ccode\u003econtents: read\u003c/code\u003e permission is also needed for \u003ccode\u003eactions/checkout@v4\u003c/code\u003e.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"inputs\"\u003eInputs\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eInput\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003escan-type\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efs\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan type to run: \u003ccode\u003efs\u003c/code\u003e, \u003ccode\u003egit\u003c/code\u003e, or \u003ccode\u003eimage\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epath\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e.\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePath to scan (for \u003ccode\u003efs\u003c/code\u003e/\u003ccode\u003egit\u003c/code\u003e) or image reference (for \u003ccode\u003eimage\u003c/code\u003e).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eformat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003esarif\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOutput format: \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, or \u003ccode\u003egithub\u003c/code\u003e (inline pull-request annotations).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eoutput\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e``\u003c/td\u003e\n\u003ctd\u003eWrite formatted output to this file (relative to \u003ccode\u003eworking-directory\u003c/code\u003e). Ignored for \u003ccode\u003eformat: github\u003c/code\u003e. When empty and \u003ccode\u003eformat: sarif\u003c/code\u003e, defaults to \u003ccode\u003eresults.sarif\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eonly-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eReport only findings confirmed active by live verification.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eno-verify\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003etrue\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDisable secret verification (no outbound calls to providers).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003emin-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMinimum severity to report: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e, or \u003ccode\u003ecritical\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eremediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInclude remediation guidance in the output.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003econfig\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e``\u003c/td\u003e\n\u003ctd\u003ePath to a \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e configuration file.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003escan-diff\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eauto\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFor \u003ccode\u003egit\u003c/code\u003e scans, scan only commits new to the event. \u003ccode\u003eauto\u003c/code\u003e enables this on \u003ccode\u003epull_request\u003c/code\u003e/\u003ccode\u003epush\u003c/code\u003e, \u003ccode\u003etrue\u003c/code\u003e forces it, \u003ccode\u003efalse\u003c/code\u003e always scans full history. Requires \u003ccode\u003eactions/checkout\u003c/code\u003e with \u003ccode\u003efetch-depth: 0\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eextra-args\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e``\u003c/td\u003e\n\u003ctd\u003eAdditional raw arguments appended to the \u003ccode\u003eleakwatch scan\u003c/code\u003e command (space-separated). Flags the action manages itself (\u003ccode\u003e--format\u003c/code\u003e, \u003ccode\u003e--output\u003c/code\u003e, \u003ccode\u003e--config\u003c/code\u003e, \u003ccode\u003e--show-raw\u003c/code\u003e) are rejected — use the dedicated inputs instead.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eworking-directory\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e.\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDirectory to run the scan from.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esarif-upload\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eUpload SARIF results to GitHub Code Scanning after the scan.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003efail-on-findings\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003etrue\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFail the workflow step when findings are reported (exit code 1). When \u003ccode\u003efalse\u003c/code\u003e, a \u003ccode\u003e::warning::\u003c/code\u003e annotation is emitted instead so the scan does not block the pipeline. Hard errors (exit code ≥ 2) always fail the step regardless of this setting.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eversion\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elatest\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eLeakwatch version to install: \u003ccode\u003elatest\u003c/code\u003e, or a release tag such as \u003ccode\u003ev1.5.0\u003c/code\u003e to pin a specific release.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003erelease-repo\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eHodeTech/Leakwatch\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRepository (\u003ccode\u003eowner/name\u003c/code\u003e) to download the release binary from. Override only for forks or self-hosted mirrors.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"outputs\"\u003eOutputs\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eOutput\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003efindings-count\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e if no findings were reported; \u003ccode\u003e1\u003c/code\u003e if findings were reported. Mirrors the Leakwatch exit code.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esarif-file\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePath to the SARIF output file on the runner (set when \u003ccode\u003eformat: sarif\u003c/code\u003e).\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"verification-in-ci\"\u003eVerification in CI\u003c/h2\u003e\n\u003cp\u003eBy default, \u003ccode\u003eno-verify\u003c/code\u003e is \u003ccode\u003etrue\u003c/code\u003e — live verification is \u003cstrong\u003eoff\u003c/strong\u003e in CI. This keeps the scan fast and avoids making outbound network calls to provider APIs from CI runners, which may be behind a firewall or have rate-limited credentials.\u003c/p\u003e\n\u003cp\u003eTo enable verification in CI, set \u003ccode\u003eno-verify: \u0026quot;false\u0026quot;\u003c/code\u003e:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e- uses: HodeTech/Leakwatch@v1\n with:\n no-verify: \u0026quot;false\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-warn\"\u003e\u003cdiv class=\"callout-label\"\u003eWarning\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eEnabling verification in CI causes Leakwatch to make authenticated API calls to providers (AWS, GitHub, Stripe, etc.) for each candidate finding. Be aware of provider rate limits and ensure the runner has outbound internet access.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"how-sarif-upload-works\"\u003eHow SARIF upload works\u003c/h2\u003e\n\u003cp\u003eWhen \u003ccode\u003esarif-upload: \u0026quot;true\u0026quot;\u003c/code\u003e and \u003ccode\u003eformat: sarif\u003c/code\u003e, the action:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eTells Leakwatch to write output to \u003ccode\u003eresults.sarif\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003eAfter the scan, calls \u003ccode\u003egithub/codeql-action/upload-sarif@v3\u003c/code\u003e with \u003ccode\u003ecategory: leakwatch\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003eGitHub processes the file and surfaces findings as \u003cstrong\u003eCode Scanning alerts\u003c/strong\u003e under the repository's \u003cstrong\u003eSecurity\u003c/strong\u003e tab.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThe upload step runs with \u003ccode\u003eif: always()\u003c/code\u003e, so results are uploaded even when \u003ccode\u003efail-on-findings: \u0026quot;true\u0026quot;\u003c/code\u003e causes the scan step to set a failure.\u003c/p\u003e\n\u003ch2 id=\"using-action-outputs\"\u003eUsing action outputs\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e- name: Scan for secrets\n id: scan\n uses: HodeTech/Leakwatch@v1\n with:\n fail-on-findings: \u0026quot;false\u0026quot; # let the workflow continue\n\n- name: Print result\n run: echo \u0026quot;Findings reported: ${{ steps.scan.outputs.findings-count }}\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"pinning-a-specific-version\"\u003ePinning a specific version\u003c/h2\u003e\n\u003cp\u003eFor reproducible builds, pin \u003ccode\u003eversion\u003c/code\u003e to a specific tag:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e- uses: HodeTech/Leakwatch@v1\n with:\n version: \u0026quot;v1.5.0\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThis downloads the prebuilt \u003ccode\u003ev1.5.0\u003c/code\u003e binary from the \u003ca href=\"https://github.com/HodeTech/Leakwatch/releases\"\u003eLeakwatch releases\u003c/a\u003e and verifies its SHA-256 checksum before running. For maximum supply-chain safety you can also pin the action itself to a commit SHA, e.g. \u003ccode\u003euses: HodeTech/Leakwatch@\u0026lt;sha\u0026gt;\u003c/code\u003e.\u003c/p\u003e\n\u003ch2 id=\"scanning-only-changed-code-pull-request-diff\"\u003eScanning only changed code (pull-request diff)\u003c/h2\u003e\n\u003cp\u003eFor \u003ccode\u003egit\u003c/code\u003e scans the action can limit the scan to the commits a pull request or push actually introduces, instead of the full history. This is faster and surfaces only newly added secrets. It is controlled by \u003ccode\u003escan-diff\u003c/code\u003e (default \u003ccode\u003eauto\u003c/code\u003e) and requires a full checkout so the base commit is available locally:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003ejobs:\n leakwatch:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n with:\n fetch-depth: 0 # required so the PR base commit is present\n - uses: HodeTech/Leakwatch@v1\n with:\n scan-type: git\n path: .\n # scan-diff: auto (default) — on pull_request/push, scans base..HEAD only\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eOn a \u003ccode\u003epull_request\u003c/code\u003e event the action scans from \u003ccode\u003egithub.event.pull_request.base.sha\u003c/code\u003e; on a \u003ccode\u003epush\u003c/code\u003e event from \u003ccode\u003egithub.event.before\u003c/code\u003e. Set \u003ccode\u003escan-diff: \u0026quot;false\u0026quot;\u003c/code\u003e to always scan the full history, or \u003ccode\u003escan-diff: \u0026quot;true\u0026quot;\u003c/code\u003e to force diff mode. \u003ccode\u003escan-diff\u003c/code\u003e has no effect on \u003ccode\u003efs\u003c/code\u003e/\u003ccode\u003eimage\u003c/code\u003e scans.\u003c/p\u003e\n\u003ch2 id=\"inline-pull-request-annotations\"\u003eInline pull-request annotations\u003c/h2\u003e\n\u003cp\u003eSet \u003ccode\u003eformat: github\u003c/code\u003e to emit the findings as GitHub Actions workflow commands, which appear as inline annotations on the pull request's \u003cstrong\u003eFiles changed\u003c/strong\u003e view and in the run log:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e- uses: HodeTech/Leakwatch@v1\n with:\n format: github\n fail-on-findings: \u0026quot;false\u0026quot; # annotate without blocking, if you prefer\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eAnnotations always show the \u003cstrong\u003eredacted\u003c/strong\u003e value only — the raw secret is never written to the (often public) PR UI or logs. Use \u003ccode\u003eformat: github\u003c/code\u003e for fast, visible PR feedback, or \u003ccode\u003eformat: sarif\u003c/code\u003e with \u003ccode\u003esarif-upload: true\u003c/code\u003e to record findings as Code Scanning alerts under the \u003cstrong\u003eSecurity\u003c/strong\u003e tab.\u003c/p\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/output/output-formats\"\u003eOutput Formats\u003c/a\u003e — understanding JSON, SARIF, CSV, and table output.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/exit-codes\"\u003eExit Codes\u003c/a\u003e — how exit codes map to scan outcomes.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eHow Verification Works\u003c/a\u003e — when and how Leakwatch calls provider APIs.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/pre-commit\"\u003ePre-commit Hook\u003c/a\u003e — catch secrets before they are committed.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/other-ci\"\u003eOther CI Systems\u003c/a\u003e — GitLab CI, Jenkins, and generic shell integration.\u003c/li\u003e\n\u003c/ul\u003e\n"},"ci-cd/other-ci":{"title":"Other CI Systems","description":"Integrate Leakwatch into GitLab CI, Jenkins, Bitbucket Pipelines, and any other CI system.","html":"\u003ch1 id=\"other-ci-systems\"\u003eOther CI Systems\u003c/h1\u003e\n\u003cp\u003eBecause Leakwatch is a single static binary with no runtime dependencies, it runs in any CI environment that can execute a shell command — GitLab CI, Jenkins, Bitbucket Pipelines, CircleCI, Azure DevOps, and others. There is no built-in integration for these systems beyond what is described on this page; the pattern is always: install the binary, run the scan, act on the exit code.\u003c/p\u003e\n\u003ch2 id=\"installing-leakwatch-in-ci\"\u003eInstalling Leakwatch in CI\u003c/h2\u003e\n\u003cp\u003eChoose the method that best suits your runner environment:\u003c/p\u003e\n\u003ch3 id=\"via-go-install-requires-go-on-the-runner\"\u003evia \u003ccode\u003ego install\u003c/code\u003e (requires Go on the runner)\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003ego install github.com/HodeTech/leakwatch@latest\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003ePin to a specific version for reproducible builds:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003ego install github.com/HodeTech/leakwatch@v1.5.0\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"via-the-docker-image-no-go-required\"\u003evia the Docker image (no Go required)\u003c/h3\u003e\n\u003cp\u003eUse \u003ccode\u003eghcr.io/hodetech/leakwatch:latest\u003c/code\u003e as a job image or run it with \u003ccode\u003edocker run\u003c/code\u003e. See \u003ca href=\"#/ci-cd/docker-usage\"\u003eDocker Usage\u003c/a\u003e for the full pattern.\u003c/p\u003e\n\u003ch3 id=\"via-a-prebuilt-release-binary\"\u003evia a prebuilt release binary\u003c/h3\u003e\n\u003cp\u003eDownload the appropriate tarball from \u003ca href=\"https://github.com/HodeTech/Leakwatch/releases\"\u003eGitHub Releases\u003c/a\u003e, extract, and place on \u003ccode\u003ePATH\u003c/code\u003e:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003ecurl -LO https://github.com/HodeTech/Leakwatch/releases/latest/download/leakwatch_Linux_amd64.tar.gz\ntar -xzf leakwatch_Linux_amd64.tar.gz\nsudo mv leakwatch /usr/local/bin/leakwatch\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"exit-codes\"\u003eExit codes\u003c/h2\u003e\n\u003cp\u003eLeakwatch exits with one of four codes, which is the primary mechanism for failing a CI build:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eCode\u003c/th\u003e\n\u003cth\u003eMeaning\u003c/th\u003e\n\u003cth\u003eRecommended CI action\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNo findings\u003c/td\u003e\n\u003ctd\u003ePass the pipeline stage\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSecrets found\u003c/td\u003e\n\u003ctd\u003eFail the pipeline stage\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHard error (bad config, unreadable path, etc.)\u003c/td\u003e\n\u003ctd\u003eFail the pipeline stage\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInterrupted (\u003ccode\u003eCtrl+C\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e) before completing, with no findings reported\u003c/td\u003e\n\u003ctd\u003eFail the pipeline stage — the scan did not run to completion, so a clean result cannot be trusted\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eA generic shell snippet that branches on the exit code:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eset +e\nleakwatch scan fs . --format json -o leakwatch.json --no-verify\nEXIT_CODE=$?\nset -e\n\nif [ \u0026quot;$EXIT_CODE\u0026quot; -eq 0 ]; then\n echo \u0026quot;No secrets found.\u0026quot;\nelif [ \u0026quot;$EXIT_CODE\u0026quot; -eq 1 ]; then\n echo \u0026quot;Secrets found — failing build.\u0026quot;\n exit 1\nelse\n echo \u0026quot;Scan error or interruption (exit $EXIT_CODE) — failing build.\u0026quot;\n exit \u0026quot;$EXIT_CODE\u0026quot;\nfi\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"gitlab-ci-example\"\u003eGitLab CI example\u003c/h2\u003e\n\u003cp\u003eThe following \u003ccode\u003e.gitlab-ci.yml\u003c/code\u003e job installs Leakwatch, runs a filesystem scan, and stores the JSON report as a pipeline artifact:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003eleakwatch:\n stage: test\n image: golang:1.25-alpine\n script:\n - go install github.com/HodeTech/leakwatch@v1.5.0\n - leakwatch scan fs . --format json -o leakwatch.json --no-verify\n artifacts:\n when: always\n paths:\n - leakwatch.json\n expire_in: 7 days\n allow_failure: false\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003eallow_failure: false\u003c/code\u003e (the default) means exit code \u003ccode\u003e1\u003c/code\u003e fails the pipeline stage. Set \u003ccode\u003eallow_failure: true\u003c/code\u003e if you want the scan to report without blocking the merge.\u003c/p\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eTip\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eGitLab supports SAST report artifacts. Leakwatch produces SARIF (\u003ccode\u003e--format sarif\u003c/code\u003e), not GitLab's native SAST JSON schema, so use the \u003ccode\u003epaths:\u003c/code\u003e artifact approach rather than the \u003ccode\u003ereports: sast:\u003c/code\u003e key.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"recommendations-for-ci-runners\"\u003eRecommendations for CI runners\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eUse \u003ccode\u003e--no-verify\u003c/code\u003e on runners without outbound internet access.\u003c/strong\u003e Verification makes live API calls to providers (AWS, GitHub, Stripe, etc.). On air-gapped or firewall-restricted runners, these calls time out and slow the scan. Pass \u003ccode\u003e--no-verify\u003c/code\u003e to skip verification entirely:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --no-verify --format sarif -o results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003cstrong\u003eSave output as an artifact.\u003c/strong\u003e Use \u003ccode\u003e--format sarif\u003c/code\u003e or \u003ccode\u003e--format json\u003c/code\u003e with \u003ccode\u003e--output\u003c/code\u003e to write a file that can be stored, uploaded to a vulnerability management platform, or reviewed after the job completes.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSet \u003ccode\u003e--min-severity\u003c/code\u003e\u003c/strong\u003e to focus on the secrets that matter most. In a noisy codebase, start with \u003ccode\u003e--min-severity high\u003c/code\u003e and lower the threshold once you have cleared the backlog.\u003c/p\u003e\n\u003ch2 id=\"azure-devops-example\"\u003eAzure DevOps example\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e- script: |\n go install github.com/HodeTech/leakwatch@v1.5.0\n leakwatch scan fs . --format sarif -o $(Build.ArtifactStagingDirectory)/leakwatch.sarif --no-verify\n displayName: \u0026quot;Leakwatch secret scan\u0026quot;\n\n- task: PublishBuildArtifacts@1\n inputs:\n pathToPublish: \u0026quot;$(Build.ArtifactStagingDirectory)\u0026quot;\n artifactName: \u0026quot;leakwatch-results\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"jenkins-example\"\u003eJenkins example\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-groovy\"\u003estage('Secret scan') {\n steps {\n sh '''\n go install github.com/HodeTech/leakwatch@v1.5.0\n leakwatch scan fs . --format json -o leakwatch.json --no-verify\n '''\n archiveArtifacts artifacts: 'leakwatch.json', allowEmptyArchive: true\n }\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/reference/exit-codes\"\u003eExit Codes\u003c/a\u003e — full reference for all exit code meanings.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/output/output-formats\"\u003eOutput Formats\u003c/a\u003e — JSON, SARIF, CSV, table, and GitHub annotation output.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/docker-usage\"\u003eDocker Usage\u003c/a\u003e — use the container image instead of installing the binary.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/github-action\"\u003eGitHub Action\u003c/a\u003e — the official action for GitHub workflows.\u003c/li\u003e\n\u003c/ul\u003e\n"},"ci-cd/pre-commit":{"title":"Pre-commit Hook","description":"Use the Leakwatch pre-commit hook to scan for secrets before every commit.","html":"\u003ch1 id=\"pre-commit-hook\"\u003ePre-commit Hook\u003c/h1\u003e\n\u003cp\u003eThe cheapest time to catch a secret is before it enters the repository at all. Leakwatch ships a native \u003ca href=\"https://pre-commit.com\"\u003epre-commit\u003c/a\u003e hook that runs \u003ccode\u003eleakwatch scan fs\u003c/code\u003e automatically on every \u003ccode\u003egit commit\u003c/code\u003e, so a leaked API key or password fails the commit rather than appearing in history.\u003c/p\u003e\n\u003ch2 id=\"prerequisites\"\u003ePrerequisites\u003c/h2\u003e\n\u003cp\u003eYou need:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003ePython 3.8+ (pre-commit is a Python tool).\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://pre-commit.com/#install\"\u003epre-commit\u003c/a\u003e installed globally (\u003ccode\u003epip install pre-commit\u003c/code\u003e or \u003ccode\u003ebrew install pre-commit\u003c/code\u003e).\u003c/li\u003e\n\u003cli\u003eGo 1.25+ on \u003ccode\u003ePATH\u003c/code\u003e — the hook language is \u003ccode\u003egolang\u003c/code\u003e, so pre-commit compiles Leakwatch from source on first run.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"configuration\"\u003eConfiguration\u003c/h2\u003e\n\u003cp\u003eAdd a \u003ccode\u003e.pre-commit-config.yaml\u003c/code\u003e file to the root of your repository (or extend an existing one):\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003erepos:\n - repo: https://github.com/HodeTech/Leakwatch\n rev: v1.5.0\n hooks:\n - id: leakwatch\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eInstall the hooks into the local Git repo:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003epre-commit install\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThat is all. From this point on, every \u003ccode\u003egit commit\u003c/code\u003e triggers a filesystem scan. If Leakwatch finds any secrets, the commit is blocked and the findings are printed to the terminal.\u003c/p\u003e\n\u003ch2 id=\"running-manually\"\u003eRunning manually\u003c/h2\u003e\n\u003cp\u003eTo scan the entire repository (not just staged files) at any time:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003epre-commit run --all-files\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eTo run only the Leakwatch hook without triggering others:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003epre-commit run leakwatch --all-files\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"passing-extra-arguments\"\u003ePassing extra arguments\u003c/h2\u003e\n\u003cp\u003eThe hook's default behavior matches \u003ccode\u003eleakwatch scan fs\u003c/code\u003e with no additional flags. You can pass extra arguments via the \u003ccode\u003eargs:\u003c/code\u003e key:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003erepos:\n - repo: https://github.com/HodeTech/Leakwatch\n rev: v1.5.0\n hooks:\n - id: leakwatch\n args:\n - --only-verified\n - --min-severity\n - high\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThis example reports only high-severity secrets that Leakwatch has confirmed are still active — a strict policy suitable for teams that want to avoid false-positive noise without sacrificing coverage.\u003c/p\u003e\n\u003cp\u003eOther useful arguments:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003eargs:\n - --no-verify # skip live verification for faster commits\n - --min-severity\n - medium # suppress low-severity noise\n - --format\n - table # human-readable output in the terminal\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNote\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003e\u003ccode\u003epass_filenames: false\u003c/code\u003e is set in the hook definition, which means the hook always scans the full working tree rather than only the files staged for the current commit. This guarantees that secrets already present in unstaged files are also detected.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"what-the-hook-scans\"\u003eWhat the hook scans\u003c/h2\u003e\n\u003cp\u003eThe hook runs \u003ccode\u003eleakwatch scan fs\u003c/code\u003e against the repository working directory. It uses the same detection pipeline as the CLI: Aho-Corasick pre-filtering, regex validation, entropy calculation, and (unless \u003ccode\u003e--no-verify\u003c/code\u003e is set) live verification.\u003c/p\u003e\n\u003cp\u003eConfiguration in \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e is respected automatically — exclusion patterns, entropy thresholds, and verification settings all apply without any extra hook configuration.\u003c/p\u003e\n\u003ch2 id=\"skipping-the-hook-temporarily\"\u003eSkipping the hook temporarily\u003c/h2\u003e\n\u003cp\u003eTo commit without running the hook (for example, when committing a controlled test fixture that contains a redacted secret):\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eSKIP=leakwatch git commit -m \u0026quot;chore: add test fixture\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-warn\"\u003e\u003cdiv class=\"callout-label\"\u003eWarning\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eUsing \u003ccode\u003eSKIP=leakwatch\u003c/code\u003e bypasses all secret scanning for that commit. Use it only when you have confirmed the content is safe, and prefer \u003ccode\u003e.leakwatchignore\u003c/code\u003e or inline \u003ccode\u003eleakwatch:ignore\u003c/code\u003e comments for permanent suppressions instead.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"keeping-the-hook-version-pinned\"\u003eKeeping the hook version pinned\u003c/h2\u003e\n\u003cp\u003ePin \u003ccode\u003erev:\u003c/code\u003e to a specific tag rather than a branch name. This ensures all developers on the team use the same detector set and the hook does not silently upgrade mid-sprint:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003erev: v1.5.0 # pin; do not use 'main' or 'HEAD'\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eUpdate by running:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003epre-commit autoupdate\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003ewhich bumps \u003ccode\u003erev\u003c/code\u003e to the latest tag and lets you review the change before committing it.\u003c/p\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/filesystem\"\u003eFilesystem Scanning\u003c/a\u003e — the underlying scan command the hook runs.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eConfiguration File\u003c/a\u003e — control exclusions, entropy, and verification in \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/github-action\"\u003eGitHub Action\u003c/a\u003e — scan on every push and pull request in GitHub CI.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/exit-codes\"\u003eExit Codes\u003c/a\u003e — how exit codes map to scan outcomes.\u003c/li\u003e\n\u003c/ul\u003e\n"},"configuration/config-file":{"title":"Configuration File","description":"How to configure Leakwatch with .leakwatch.yaml — full schema, defaults, validation rules, environment overrides, and the leakwatch init command.","html":"\u003ch1 id=\"configuration-file\"\u003eConfiguration File\u003c/h1\u003e\n\u003cp\u003eLeakwatch's behaviour across every scan command is driven by a single YAML file named \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e. Understanding this file lets you tune concurrency, verification, output format, and path filtering once — and have every scan pick it up automatically.\u003c/p\u003e\n\u003ch2 id=\"file-discovery\"\u003eFile discovery\u003c/h2\u003e\n\u003cp\u003eLeakwatch resolves the config file in the following order:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003e--config \u0026lt;path\u0026gt;\u003c/code\u003e flag\u003c/strong\u003e — use an explicit path regardless of the working directory.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCurrent directory\u003c/strong\u003e — \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e in the directory where the command is run.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eHome directory\u003c/strong\u003e — \u003ccode\u003e~/.leakwatch.yaml\u003c/code\u003e as a fallback.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eIf no file is found, built-in defaults are used for every setting. If a file \u003cstrong\u003eis\u003c/strong\u003e found (or \u003ccode\u003e--config\u003c/code\u003e points at one) but fails to parse as valid YAML, that is a fatal error — the scan does not silently fall back to defaults, since doing so could silently widen detection scope beyond what the operator configured.\u003c/p\u003e\n\u003ch2 id=\"generating-a-starter-file\"\u003eGenerating a starter file\u003c/h2\u003e\n\u003cp\u003eThe \u003ccode\u003eleakwatch init\u003c/code\u003e command writes a ready-to-edit file with recommended defaults:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch init\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBy default the file is written to \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e in the current directory. Use \u003ccode\u003e--output\u003c/code\u003e to choose a different path:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch init --output /etc/leakwatch/.leakwatch.yaml\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eIf the target file already exists, \u003ccode\u003eleakwatch init\u003c/code\u003e will refuse to overwrite it and exit with an error. Pass \u003ccode\u003e--force\u003c/code\u003e to overwrite:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch init --force\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"environment-variable-overrides\"\u003eEnvironment variable overrides\u003c/h2\u003e\n\u003cp\u003eEvery config key can be overridden with an environment variable. The naming rule is:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003ePrefix: \u003ccode\u003eLEAKWATCH_\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eReplace \u003ccode\u003e.\u003c/code\u003e and \u003ccode\u003e-\u003c/code\u003e with \u003ccode\u003e_\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eUppercase\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eExamples:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eConfig key\u003c/th\u003e\n\u003cth\u003eEnvironment variable\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003escan.concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_SCAN_CONCURRENCY\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003everification.rate-limit\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_VERIFICATION_RATE_LIMIT\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eoutput.format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_OUTPUT_FORMAT\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edetection.entropy.threshold\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_DETECTION_ENTROPY_THRESHOLD\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"precedence\"\u003ePrecedence\u003c/h2\u003e\n\u003cp\u003eWhen the same setting is specified in multiple places, the highest-priority source wins:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eCommand-line flag (highest)\u003c/li\u003e\n\u003cli\u003eEnvironment variable\u003c/li\u003e\n\u003cli\u003eConfig file value\u003c/li\u003e\n\u003cli\u003eBuilt-in default (lowest)\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"full-schema\"\u003eFull schema\u003c/h2\u003e\n\u003cp\u003eThe annotated schema below shows every supported key, its default value, and valid range.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e# ── Scan engine ──────────────────────────────────────────────────────────────\n\nscan:\n # Number of concurrent file-processing workers.\n # Defaults to the number of logical CPU cores on the host.\n # Must be \u0026gt;= 1.\n concurrency: 8\n\n # Maximum file size to scan, in bytes. Files larger than this limit are\n # skipped entirely. Default is 10 MB (10485760). Must be \u0026gt;= 1.\n max-file-size: 10485760\n\n# ── Detection ─────────────────────────────────────────────────────────────────\n\ndetection:\n entropy:\n # Enable Shannon entropy calculation for each candidate match.\n enabled: true\n\n # Entropy threshold used for display, and to gate the built-in\n # generic-api-key detector plus any custom rule with its own entropy field.\n # Range: 0–8. Default: 4.0.\n # See note below about built-in findings.\n threshold: 4.0\n\n# ── Verification ─────────────────────────────────────────────────────────────\n\nverification:\n # Enable live verification against provider APIs.\n enabled: true\n\n # Per-request HTTP timeout. Must be \u0026gt;= 1ms when verification is enabled.\n # Use a duration string (e.g. \u0026quot;10s\u0026quot;, \u0026quot;500ms\u0026quot;) — a bare integer is\n # treated as nanoseconds and will fail validation.\n timeout: 10s\n\n # Number of concurrent verification workers. Must be \u0026gt;= 1.\n concurrency: 4\n\n # Maximum verification requests per second (token-bucket rate limiter).\n # Must be \u0026gt; 0.\n rate-limit: 10.0\n\n# ── Filtering ─────────────────────────────────────────────────────────────────\n\nfilter:\n # Glob patterns for paths to exclude from scanning.\n # Supported glob styles: filepath.Match patterns, ** double-star spanning\n # zero or more path segments, and trailing-slash dir/ patterns that match\n # the named directory at any depth. Each pattern is tested against both the\n # full path and the base filename, so simple patterns like \u0026quot;*.min.js\u0026quot; match\n # nested files without a leading path prefix.\n # Applies to all scan sources. (The --exclude flag, available on every scan\n # subcommand except `scan slack`, adds to this list at run time rather than\n # replacing it.)\n # Default: [] (no exclusions beyond the built-in binary/lock-file skips).\n exclude-paths:\n - \u0026quot;vendor/**\u0026quot;\n - \u0026quot;node_modules/**\u0026quot;\n - \u0026quot;**/*.min.js\u0026quot;\n - \u0026quot;**/*.min.css\u0026quot;\n - \u0026quot;go.sum\u0026quot;\n - \u0026quot;package-lock.json\u0026quot;\n - \u0026quot;yarn.lock\u0026quot;\n\n # Detector IDs to disable entirely. Findings from listed detectors are never\n # produced regardless of other settings. Default: [].\n # The --exclude-detectors flag, available on every scan subcommand, adds to\n # this list at run time rather than replacing it.\n exclude-detectors: []\n\n# ── Output ────────────────────────────────────────────────────────────────────\n\noutput:\n # Output format. One of: json, sarif, csv, table, github. Default: json.\n # The --format / -f flag overrides this at run time.\n format: json\n\n # Write output to this file path instead of stdout. Default: \u0026quot;\u0026quot; (stdout).\n # The --output / -o flag overrides this at run time. A bare path with no\n # extension is auto-suffixed with the format's own extension, and the file\n # is written with 0600 permissions. Ignored when format is \u0026quot;github\u0026quot;, which\n # always writes to stdout.\n file: \u0026quot;\u0026quot;\n\n # Drop findings below this severity level.\n # One of: low, medium, high, critical. Default: \u0026quot;\u0026quot; (show all).\n # The --min-severity flag overrides this at run time.\n severity-threshold: \u0026quot;\u0026quot;\n\n # Include the unredacted secret value in output.\n # Default: false. The --show-raw flag overrides this at run time.\n show-raw: false\n\n# ── Custom rules ──────────────────────────────────────────────────────────────\n\n# Define your own detectors as YAML rules. See the custom rules page for the\n# full rule schema.\n# custom-rules:\n# - id: \u0026quot;my-internal-token\u0026quot;\n# description: \u0026quot;Internal Service Token\u0026quot;\n# regex: \u0026quot;mycompany_[a-zA-Z0-9]{32}\u0026quot;\n# keywords: [\u0026quot;mycompany_\u0026quot;]\n# severity: critical\ncustom-rules: []\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNote\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003e\u003ccode\u003edetection.entropy.threshold\u003c/code\u003e controls which entropy value is displayed alongside a finding, and gates any detector that opts into entropy-based heuristics — currently only the built-in \u003ccode\u003egeneric-api-key\u003c/code\u003e detector, plus custom rules that declare their own \u003ccode\u003eentropy\u003c/code\u003e field. A match from one of these whose entropy falls below the threshold is suppressed as a likely placeholder. Every other (structural/format-anchored) built-in detector — \u003ccode\u003eaws-access-key-id\u003c/code\u003e, \u003ccode\u003egithub-token\u003c/code\u003e, and the rest — has its own fixed match criteria and is \u003cstrong\u003enever\u003c/strong\u003e dropped by this setting, regardless of entropy.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"validation\"\u003eValidation\u003c/h2\u003e\n\u003cp\u003eLeakwatch validates the loaded configuration before starting a scan and exits with an error for any of the following:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eCondition\u003c/th\u003e\n\u003cth\u003eError\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003escan.concurrency \u0026lt; 1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInvalid concurrency value\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003escan.max-file-size \u0026lt; 1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInvalid max-file-size value\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eoutput.format\u003c/code\u003e not in \u003ccode\u003ejson|sarif|csv|table|github\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eUnsupported output format\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edetection.entropy.threshold\u003c/code\u003e outside 0–8\u003c/td\u003e\n\u003ctd\u003eInvalid entropy threshold\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eoutput.severity-threshold\u003c/code\u003e not a valid level (when non-empty)\u003c/td\u003e\n\u003ctd\u003eInvalid severity-threshold\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003everification.timeout \u0026lt; 1ms\u003c/code\u003e (when verification enabled)\u003c/td\u003e\n\u003ctd\u003eInvalid verification timeout\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003everification.concurrency \u0026lt; 1\u003c/code\u003e (when verification enabled)\u003c/td\u003e\n\u003ctd\u003eInvalid verification concurrency\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003everification.rate-limit \u0026lt;= 0\u003c/code\u003e (when verification enabled)\u003c/td\u003e\n\u003ctd\u003eInvalid verification rate-limit\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/ignoring-findings\"\u003eIgnoring Findings\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/severity-and-filtering\"\u003eSeverity \u0026amp; Filtering\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/detectors/custom-rules\"\u003eCustom Rules\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/environment-variables\"\u003eEnvironment Variables\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n"},"configuration/ignoring-findings":{"title":"Ignoring Findings","description":"Suppress false positives with .leakwatchignore files, inline ignore markers, and built-in binary and lock-file skips.","html":"\u003ch1 id=\"ignoring-findings\"\u003eIgnoring Findings\u003c/h1\u003e\n\u003cp\u003eNo scanner has zero false positives. Leakwatch gives you three layered mechanisms to suppress the noise: a \u003ccode\u003e.leakwatchignore\u003c/code\u003e file for path-based exclusions, inline markers for line-level suppression, and a set of always-on built-in skips for binary files and common lock files.\u003c/p\u003e\n\u003ch2 id=\"leakwatchignore-file\"\u003e\u003ccode\u003e.leakwatchignore\u003c/code\u003e file\u003c/h2\u003e\n\u003cp\u003eCreate a \u003ccode\u003e.leakwatchignore\u003c/code\u003e file in your repository root (or in the current directory) to exclude paths from the scan results. It uses a gitignore-style syntax:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eLines starting with \u003ccode\u003e#\u003c/code\u003e are comments.\u003c/li\u003e\n\u003cli\u003eBlank lines are skipped.\u003c/li\u003e\n\u003cli\u003eA \u003ccode\u003e!\u003c/code\u003e prefix \u003cstrong\u003enegates\u003c/strong\u003e a pattern, re-including a path that a previous pattern would have excluded.\u003c/li\u003e\n\u003cli\u003eThe \u003cstrong\u003elast matching pattern wins\u003c/strong\u003e — order matters.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"loading-order\"\u003eLoading order\u003c/h3\u003e\n\u003cp\u003eLeakwatch loads the \u003cstrong\u003efirst\u003c/strong\u003e \u003ccode\u003e.leakwatchignore\u003c/code\u003e it finds, checking the scan root first, then the current working directory as a fallback. Only one file is ever used per scan — if a \u003ccode\u003e.leakwatchignore\u003c/code\u003e exists at the scan root, the current-directory file (if any) is never opened, let alone merged. There is no cross-file precedence or merging: place your rules in a single \u003ccode\u003e.leakwatchignore\u003c/code\u003e at the scan root for predictable behavior.\u003c/p\u003e\n\u003ch3 id=\"glob-syntax\"\u003eGlob syntax\u003c/h3\u003e\n\u003cp\u003eThree pattern styles are supported:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eStyle\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003cth\u003eExample\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003eStandard glob\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efilepath.Match\u003c/code\u003e-style, matched against both the full path and the base filename\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e*.pem\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eDouble-star \u003ccode\u003e**\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSpans zero or more path segments\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003etest/fixtures/**\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eTrailing slash \u003ccode\u003edir/\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMatches every file inside the named directory at any depth\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003esnapshots/\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"example-leakwatchignore\"\u003eExample \u003ccode\u003e.leakwatchignore\u003c/code\u003e\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003e# Ignore all test fixture files\ntest/fixtures/**\n\n# Ignore known placeholder keys in documentation\ndocs/examples/\n\n# Ignore files with a specific extension anywhere in the tree\n*.pem.example\n\n# Re-include a specific file excluded by the rule above\n!docs/examples/real-config-sample.yaml\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNote\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003e\u003ccode\u003e.leakwatchignore\u003c/code\u003e filtering is applied \u003cstrong\u003eafter\u003c/strong\u003e the scan completes, based on the file path of each finding. It does not prevent files from being read — it suppresses the findings they produce. To skip files before they are read at all, use \u003ccode\u003efilter.exclude-paths\u003c/code\u003e in the config file or \u003ccode\u003e--exclude\u003c/code\u003e on \u003ccode\u003escan fs\u003c/code\u003e.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"inline-ignore-markers\"\u003eInline ignore markers\u003c/h2\u003e\n\u003cp\u003ePlace a marker directly on any source line to suppress detectors for that specific line. The marker can appear anywhere on the line — typically inside a comment — and is applied by the engine \u003cstrong\u003ebefore\u003c/strong\u003e verification, so an ignored line never triggers a network call.\u003c/p\u003e\n\u003ch3 id=\"suppress-all-detectors-on-a-line\"\u003eSuppress all detectors on a line\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-python\"\u003e# Payment processing configuration\nSTRIPE_KEY = \u0026quot;sk_test_XXXXXXXXXXXXXXXXXXXX\u0026quot; # leakwatch:ignore\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"suppress-a-specific-detector-on-a-line\"\u003eSuppress a specific detector on a line\u003c/h3\u003e\n\u003cp\u003eUse \u003ccode\u003eleakwatch:ignore:\u0026lt;detector-id\u0026gt;\u003c/code\u003e to suppress only one detector while leaving others active:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-go\"\u003e// This token is intentionally a placeholder for documentation\nexampleToken := \u0026quot;ghp_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\u0026quot; // leakwatch:ignore:github-token\n\u003c/code\u003e\u003c/pre\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e# CI environment variable set by the platform — not a real secret\napi_key: \u0026quot;${CI_API_KEY_PLACEHOLDER}\u0026quot; # leakwatch:ignore:generic-api-key\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eTip\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003ePrefer the detector-specific form (\u003ccode\u003eleakwatch:ignore:\u0026lt;detector-id\u0026gt;\u003c/code\u003e) over the generic one whenever possible. It documents which detector you are suppressing and keeps all other detectors active on that line.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"built-in-skips-always-applied\"\u003eBuilt-in skips (always applied)\u003c/h2\u003e\n\u003cp\u003eLeakwatch unconditionally skips the following before running any detector:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eBinary file extensions\u003c/strong\u003e — files with extensions such as \u003ccode\u003e.exe\u003c/code\u003e, \u003ccode\u003e.dll\u003c/code\u003e, \u003ccode\u003e.so\u003c/code\u003e, \u003ccode\u003e.dylib\u003c/code\u003e, \u003ccode\u003e.bin\u003c/code\u003e, \u003ccode\u003e.png\u003c/code\u003e, \u003ccode\u003e.jpg\u003c/code\u003e, \u003ccode\u003e.gif\u003c/code\u003e, \u003ccode\u003e.mp4\u003c/code\u003e, \u003ccode\u003e.zip\u003c/code\u003e, \u003ccode\u003e.tar\u003c/code\u003e, \u003ccode\u003e.gz\u003c/code\u003e, \u003ccode\u003e.pdf\u003c/code\u003e, \u003ccode\u003e.woff\u003c/code\u003e, \u003ccode\u003e.ttf\u003c/code\u003e, and others are never scanned.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eBinary content detection\u003c/strong\u003e — any file whose first 8 KB contains a null byte is treated as binary and skipped, regardless of extension.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eCommon lock files\u003c/strong\u003e — the following filenames are always skipped because they contain hashes and checksums that produce high rates of false positives:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFile\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epackage-lock.json\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eyarn.lock\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epnpm-lock.yaml\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecomposer.lock\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eGemfile.lock\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eCargo.lock\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epoetry.lock\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ego.sum\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ePipfile.lock\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eThese built-in skips cannot be disabled. They are separate from the \u003ccode\u003efilter.exclude-paths\u003c/code\u003e setting and run before any config-based filtering.\u003c/p\u003e\n\u003ch2 id=\"path-based-exclusion-before-scanning\"\u003ePath-based exclusion before scanning\u003c/h2\u003e\n\u003cp\u003eTo exclude paths before they are even read by the scan engine, use \u003ccode\u003efilter.exclude-paths\u003c/code\u003e in your config file:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003efilter:\n exclude-paths:\n - \u0026quot;vendor/**\u0026quot;\n - \u0026quot;node_modules/**\u0026quot;\n - \u0026quot;**/*.min.js\u0026quot;\n - \u0026quot;third-party/\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThis setting applies to \u003cstrong\u003eall scan sources\u003c/strong\u003e (filesystem, Git history, container images, cloud storage, Slack). On every \u003ccode\u003escan\u003c/code\u003e subcommand that scans paths (\u003ccode\u003efs\u003c/code\u003e, \u003ccode\u003egit\u003c/code\u003e, \u003ccode\u003eimage\u003c/code\u003e, \u003ccode\u003es3\u003c/code\u003e, \u003ccode\u003egcs\u003c/code\u003e, \u003ccode\u003erepos\u003c/code\u003e — all except \u003ccode\u003eslack\u003c/code\u003e, which excludes by \u003ccode\u003e--exclude-channels\u003c/code\u003e instead) you can also pass \u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e on the command line, repeatable, which is combined with (not a replacement for) \u003ccode\u003efilter.exclude-paths\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003eTo silence a noisy detector for a single run without editing the config file, pass \u003ccode\u003e--exclude-detectors \u0026lt;id\u0026gt;[,\u0026lt;id\u0026gt;...]\u003c/code\u003e (repeatable) on any scan subcommand — see \u003ca href=\"#/detectors/detector-catalog\"\u003eDetector Catalog\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eSee \u003ca href=\"#/configuration/config-file\"\u003eConfiguration File\u003c/a\u003e for the full config schema and \u003ca href=\"#/configuration/severity-and-filtering\"\u003eSeverity \u0026amp; Filtering\u003c/a\u003e for detector-level and severity-level filtering.\u003c/p\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eConfiguration File\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/severity-and-filtering\"\u003eSeverity \u0026amp; Filtering\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n"},"configuration/severity-and-filtering":{"title":"Severity \u0026 Filtering","description":"Control which findings reach your output using severity thresholds, verified-only mode, detector exclusions, and path exclusions.","html":"\u003ch1 id=\"severity--filtering\"\u003eSeverity \u0026amp; Filtering\u003c/h1\u003e\n\u003cp\u003eA busy codebase can produce many findings. Leakwatch provides several independent filters you can combine to focus on the signals that matter most: severity thresholds drop low-priority noise, verified-only mode surfaces only confirmed live secrets, detector exclusions silence known false-positive sources, and path exclusions remove entire directory trees from scope.\u003c/p\u003e\n\u003ch2 id=\"severity-levels\"\u003eSeverity levels\u003c/h2\u003e\n\u003cp\u003eEvery built-in detector ships with a default severity. The four levels, from lowest to highest priority, are:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eLevel\u003c/th\u003e\n\u003cth\u003eTypical use\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGeneric patterns with a higher false-positive rate\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003emedium\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRecognizable credential formats, unconfirmed\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ehigh\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eWell-structured secrets where exposure is likely significant\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecritical\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eLive secrets confirmed or formats with near-zero false-positive rates\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eThe severity assigned to each detector is listed in the \u003ca href=\"#/detectors/detector-catalog\"\u003eDetector Catalog\u003c/a\u003e.\u003c/p\u003e\n\u003ch2 id=\"--min-severity-drop-findings-below-a-threshold\"\u003e\u003ccode\u003e--min-severity\u003c/code\u003e: drop findings below a threshold\u003c/h2\u003e\n\u003cp\u003ePass \u003ccode\u003e--min-severity \u0026lt;level\u0026gt;\u003c/code\u003e to discard findings whose severity is below the specified level. Only findings at or above the threshold reach the output.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Show only high and critical findings\nleakwatch scan fs . --min-severity high\n\n# Show medium, high, and critical findings\nleakwatch scan fs . --min-severity medium\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eYou can set a persistent default in the config file under \u003ccode\u003eoutput.severity-threshold\u003c/code\u003e. The \u003ccode\u003e--min-severity\u003c/code\u003e flag overrides the config value at run time:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003eoutput:\n severity-threshold: medium\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"--only-verified-confirmed-active-secrets-only\"\u003e\u003ccode\u003e--only-verified\u003c/code\u003e: confirmed active secrets only\u003c/h2\u003e\n\u003cp\u003ePass \u003ccode\u003e--only-verified\u003c/code\u003e to keep only findings whose verification status is \u003ccode\u003everified_active\u003c/code\u003e — secrets that Leakwatch confirmed are still valid by making a controlled read-only call to the provider API. All other findings (\u003ccode\u003eunverified\u003c/code\u003e, \u003ccode\u003everified_inactive\u003c/code\u003e, or \u003ccode\u003everify_error\u003c/code\u003e) are dropped.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --only-verified\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThis flag is most useful in CI pipelines where you want to fail the build \u003cstrong\u003eonly\u003c/strong\u003e on confirmed incidents, not on suspicious patterns that may be placeholders or already-rotated credentials.\u003c/p\u003e\n\u003cp\u003eSee \u003ca href=\"#/verification/how-verification-works\"\u003eHow Verification Works\u003c/a\u003e for which detectors support live verification.\u003c/p\u003e\n\u003ch2 id=\"filterexclude-detectors-disable-specific-detectors\"\u003e\u003ccode\u003efilter.exclude-detectors\u003c/code\u003e: disable specific detectors\u003c/h2\u003e\n\u003cp\u003eTo permanently disable one or more detectors, list their IDs under \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e in the config file. Findings from listed detectors are never produced, regardless of any other setting:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003efilter:\n exclude-detectors:\n - generic-api-key\n - jwt\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eDetector IDs are listed in the \u003ca href=\"#/detectors/detector-catalog\"\u003eDetector Catalog\u003c/a\u003e. Use this setting when a detector consistently produces false positives for your codebase and other suppression mechanisms (inline ignores or \u003ccode\u003e.leakwatchignore\u003c/code\u003e) are not granular enough.\u003c/p\u003e\n\u003cp\u003eTo silence a detector for a single run without editing the config file, pass \u003ccode\u003e--exclude-detectors \u0026lt;id\u0026gt;[,\u0026lt;id\u0026gt;...]\u003c/code\u003e (repeatable) on any scan subcommand. It adds to \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e rather than replacing it.\u003c/p\u003e\n\u003ch2 id=\"filterexclude-paths-skip-paths-before-scanning\"\u003e\u003ccode\u003efilter.exclude-paths\u003c/code\u003e: skip paths before scanning\u003c/h2\u003e\n\u003cp\u003eTo exclude paths before the scan engine reads them, use \u003ccode\u003efilter.exclude-paths\u003c/code\u003e in the config file. The patterns use the same glob syntax as \u003ccode\u003e.leakwatchignore\u003c/code\u003e (standard globs, \u003ccode\u003e**\u003c/code\u003e double-star, and trailing-slash directory patterns), and apply to \u003cstrong\u003eall scan sources\u003c/strong\u003e:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003efilter:\n exclude-paths:\n - \u0026quot;vendor/**\u0026quot;\n - \u0026quot;node_modules/**\u0026quot;\n - \u0026quot;**/*.min.js\u0026quot;\n - \u0026quot;**/*.min.css\u0026quot;\n - \u0026quot;test/fixtures/\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNote\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eThe \u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e flag is the command-line equivalent of \u003ccode\u003efilter.exclude-paths\u003c/code\u003e, and is available on every scan subcommand \u003cstrong\u003eexcept\u003c/strong\u003e \u003ccode\u003escan slack\u003c/code\u003e (which excludes by \u003ccode\u003e--exclude-channels\u003c/code\u003e instead). It is repeatable and adds to \u003ccode\u003efilter.exclude-paths\u003c/code\u003e rather than replacing it.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"combining-filters-in-ci\"\u003eCombining filters in CI\u003c/h2\u003e\n\u003cp\u003eIn a CI pipeline you typically want a low-noise, high-signal run that fails only on real incidents. A recommended combination:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . \\\n --only-verified \\\n --min-severity high \\\n --format sarif \\\n --output results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eWith a config file handling the persistent path exclusions:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003efilter:\n exclude-paths:\n - \u0026quot;vendor/**\u0026quot;\n - \u0026quot;node_modules/**\u0026quot;\n - \u0026quot;test/fixtures/\u0026quot;\n exclude-detectors:\n - generic-api-key\n\noutput:\n severity-threshold: high\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThen override just the format and destination at the command line for CI:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --only-verified --format sarif --output results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eSee \u003ca href=\"#/verification/how-verification-works\"\u003eHow Verification Works\u003c/a\u003e for verification details, \u003ca href=\"#/configuration/ignoring-findings\"\u003eIgnoring Findings\u003c/a\u003e for inline and file-based suppression, and \u003ca href=\"#/configuration/config-file\"\u003eConfiguration File\u003c/a\u003e for the full schema.\u003c/p\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/detectors/detector-catalog\"\u003eDetector Catalog\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eHow Verification Works\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eConfiguration File\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/ignoring-findings\"\u003eIgnoring Findings\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n"},"detectors/custom-rules":{"title":"Custom Rules","description":"How to define your own secret detection patterns in YAML and add them to a Leakwatch scan alongside the 64 built-in detectors.","html":"\u003ch1 id=\"custom-rules\"\u003eCustom Rules\u003c/h1\u003e\n\u003cp\u003eThe 64 built-in detectors cover widely used credential formats, but every organisation has internal tokens, proprietary service keys, or environment-specific patterns that no generic tool can anticipate. Custom rules let you extend Leakwatch with your own patterns — defined in plain YAML, loaded at runtime — without modifying source code or rebuilding the binary.\u003c/p\u003e\n\u003ch2 id=\"where-custom-rules-live\"\u003eWhere custom rules live\u003c/h2\u003e\n\u003cp\u003eCustom rules are defined under a top-level \u003ccode\u003ecustom-rules:\u003c/code\u003e list in your \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e configuration file:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003ecustom-rules:\n - id: acme-internal-token\n description: \u0026quot;ACME Corp internal service token\u0026quot;\n regex: 'acme_[a-z0-9]{32}'\n keywords:\n - acme_\n severity: critical\n entropy: 3.5\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThe rules are registered at runtime when Leakwatch starts. They run alongside the built-in detectors using the same Aho-Corasick pre-filter pipeline.\u003c/p\u003e\n\u003ch2 id=\"rule-fields\"\u003eRule fields\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eField\u003c/th\u003e\n\u003cth\u003eRequired\u003c/th\u003e\n\u003cth\u003eType\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eid\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYes\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003eUnique detector ID. Used in output and in \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e. Must not collide with a built-in detector ID or another custom rule ID.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edescription\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNo\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003eHuman-readable description shown in output.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eregex\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYes\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003eRE2-compatible regular expression. Maximum 4096 characters.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ekeywords\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNo\u003c/td\u003e\n\u003ctd\u003elist of strings\u003c/td\u003e\n\u003ctd\u003eAho-Corasick pre-filter keywords. The regex only runs on chunks that contain at least one of these strings. Omitting this field causes the regex to run on every chunk.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eseverity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNo\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ecritical\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, or \u003ccode\u003elow\u003c/code\u003e. Defaults to \u003ccode\u003emedium\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eentropy\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNo\u003c/td\u003e\n\u003ctd\u003efloat\u003c/td\u003e\n\u003ctd\u003eShannon entropy threshold (0–8). Matches whose entropy is \u003cstrong\u003ebelow\u003c/strong\u003e this value are discarded. Useful for filtering low-randomness false positives.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eTip\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eAlways supply \u003ccode\u003ekeywords\u003c/code\u003e. Even a single short keyword (like a token prefix) dramatically reduces the number of chunks the regex engine processes, keeping scans fast on large repositories. For example, if all your internal tokens begin with \u003ccode\u003eacme_\u003c/code\u003e, set \u003ccode\u003ekeywords: [acme_]\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003eUse \u003ccode\u003eentropy\u003c/code\u003e to suppress matches on placeholder values like \u003ccode\u003eacme_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u003c/code\u003e that satisfy the pattern but are clearly not real secrets. A threshold around 3.0–3.5 is a good starting point.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"collision-handling\"\u003eCollision handling\u003c/h2\u003e\n\u003cp\u003eIf a custom rule's \u003ccode\u003eid\u003c/code\u003e matches an already-registered detector — either a built-in detector or a previously loaded custom rule — the duplicate is \u003cstrong\u003eskipped\u003c/strong\u003e and an error is logged. Leakwatch does not crash; the rest of the rules load normally. Check the log output if a custom rule appears to have no effect.\u003c/p\u003e\n\u003ch2 id=\"verification\"\u003eVerification\u003c/h2\u003e\n\u003cp\u003eCustom rules have no paired verifier. Findings from custom rules are always reported with status \u003ccode\u003eunverified\u003c/code\u003e — they never become \u003ccode\u003everified_active\u003c/code\u003e or \u003ccode\u003everified_inactive\u003c/code\u003e.\u003c/p\u003e\n\u003ch2 id=\"complete-example\"\u003eComplete example\u003c/h2\u003e\n\u003cp\u003eThe following \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e defines two custom rules: one for an internal service token and one for a signing secret used in webhooks.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003ecustom-rules:\n - id: acme-internal-token\n description: \u0026quot;ACME Corp internal service token (format: acme_ + 32 hex chars)\u0026quot;\n regex: 'acme_[a-f0-9]{32}'\n keywords:\n - acme_\n severity: critical\n entropy: 3.2\n\n - id: acme-webhook-signing-secret\n description: \u0026quot;ACME Corp webhook signing secret (format: whsec_ + 40 base64url chars)\u0026quot;\n regex: 'whsec_[A-Za-z0-9_\\-]{40}'\n keywords:\n - whsec_\n severity: high\n entropy: 3.5\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eRun a scan with this config:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --config .leakwatch.yaml\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eSample JSON output for a custom-rule finding (secret value redacted) — a custom-rule finding uses exactly the same \u003ccode\u003eFinding\u003c/code\u003e schema as every built-in detector (see \u003ca href=\"#/output/output-formats\"\u003eOutput Formats\u003c/a\u003e); there is no separate schema for custom rules:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-json\"\u003e{\n \u0026quot;id\u0026quot;: \u0026quot;8a403a46336fc6ffdc847d202b74536c\u0026quot;,\n \u0026quot;detector_id\u0026quot;: \u0026quot;acme-internal-token\u0026quot;,\n \u0026quot;severity\u0026quot;: \u0026quot;critical\u0026quot;,\n \u0026quot;redacted\u0026quot;: \u0026quot;****cdef\u0026quot;,\n \u0026quot;source\u0026quot;: {\n \u0026quot;source_type\u0026quot;: \u0026quot;filesystem\u0026quot;,\n \u0026quot;file_path\u0026quot;: \u0026quot;config/production.env\u0026quot;,\n \u0026quot;line\u0026quot;: 14\n },\n \u0026quot;verification\u0026quot;: {\n \u0026quot;status\u0026quot;: \u0026quot;unverified\u0026quot;\n },\n \u0026quot;detected_at\u0026quot;: \u0026quot;2026-05-23T10:15:30Z\u0026quot;,\n \u0026quot;entropy\u0026quot;: 4.12\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNote\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eThe \u003ccode\u003eredacted\u003c/code\u003e field always masks the actual secret (by default, \u003ccode\u003e****\u003c/code\u003e followed by at most the last 4 characters). The raw value is never written to output unless you explicitly pass \u003ccode\u003e--show-raw\u003c/code\u003e (not recommended outside controlled environments). Custom rules have no verifier, so \u003ccode\u003everification.status\u003c/code\u003e is always \u003ccode\u003eunverified\u003c/code\u003e.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"excluding-a-custom-rule\"\u003eExcluding a custom rule\u003c/h2\u003e\n\u003cp\u003eCustom rules participate in the same filtering as built-in detectors. To disable a custom rule without removing it from config:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003efilter:\n exclude-detectors:\n - acme-internal-token\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eConfiguration: Config File\u003c/a\u003e — full reference for \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e, including where \u003ccode\u003ecustom-rules:\u003c/code\u003e sits in the document structure.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/detectors/detector-catalog\"\u003eDetector Catalog\u003c/a\u003e — the 64 built-in detectors, to check for ID conflicts before naming your custom rule.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/how-it-works\"\u003eHow It Works\u003c/a\u003e — the Aho-Corasick pre-filter pipeline that \u003ccode\u003ekeywords\u003c/code\u003e plugs into.\u003c/li\u003e\n\u003c/ul\u003e\n"},"detectors/detector-catalog":{"title":"Detector Catalog","description":"All 64 built-in detectors grouped by category, with their IDs, what they detect, and their default severity.","html":"\u003ch1 id=\"detector-catalog\"\u003eDetector Catalog\u003c/h1\u003e\n\u003cp\u003eLeakwatch ships \u003cstrong\u003e64 built-in detectors\u003c/strong\u003e that cover a wide range of credential types — from cloud provider access keys and AI API tokens to database connection strings and private cryptographic keys. Each detector has a stable ID, a default severity, and (for most) a paired verifier that can confirm whether a found secret is still live.\u003c/p\u003e\n\u003cp\u003eThis page lists every built-in detector. For verification coverage details see \u003ca href=\"#/verification/verification-coverage\"\u003eVerification Coverage\u003c/a\u003e. To add your own patterns, see \u003ca href=\"#/detectors/custom-rules\"\u003eCustom Rules\u003c/a\u003e.\u003c/p\u003e\n\u003ch2 id=\"how-to-read-this-catalog\"\u003eHow to read this catalog\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eID\u003c/strong\u003e — the stable string identifier used in config and output. Pass it to \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e to skip a detector, or use it with \u003ccode\u003e--min-severity\u003c/code\u003e filtering (\u003ca href=\"#/configuration/severity-and-filtering\"\u003eSeverity and Filtering\u003c/a\u003e).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDetects\u003c/strong\u003e — what the detector is looking for.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSeverity\u003c/strong\u003e — \u003ccode\u003eCritical\u003c/code\u003e, \u003ccode\u003eHigh\u003c/code\u003e, or \u003ccode\u003eMedium\u003c/code\u003e. This is the default; it feeds the \u003ccode\u003e--min-severity\u003c/code\u003e flag and the \u003ccode\u003eoutput.severity-threshold\u003c/code\u003e config key.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"cloud-and-infrastructure\"\u003eCloud and Infrastructure\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eDetects\u003c/th\u003e\n\u003cth\u003eSeverity\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eaws-access-key-id\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAWS Access Key ID\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egcp-service-account\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGCP Service Account Key\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eazure-storage-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAzure Storage Connection String\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eazure-entra-secret\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAzure Entra ID Client Secret\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edigitalocean-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDigitalOcean Personal Access Token\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecloudflare-api-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCloudflare API Token\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eheroku-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHeroku API Key\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003evercel-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eVercel API Token\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eterraform-cloud-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTerraform Cloud/Enterprise API Token\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ehashicorp-vault-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHashiCorp Vault Token\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edoppler-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDoppler Token — service (\u003ccode\u003edp.st.\u003c/code\u003e), personal (\u003ccode\u003edp.pt.\u003c/code\u003e), CLI/config (\u003ccode\u003edp.ct.\u003c/code\u003e), and SCIM (\u003ccode\u003edp.scim.\u003c/code\u003e) tokens\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"ai--ml\"\u003eAI / ML\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eDetects\u003c/th\u003e\n\u003cth\u003eSeverity\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eopenai-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOpenAI API Key — covers project-scoped (\u003ccode\u003esk-proj-\u003c/code\u003e), service-account (\u003ccode\u003esk-svcacct-\u003c/code\u003e), and legacy (\u003ccode\u003esk-\u003c/code\u003e) key formats\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eanthropic-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAnthropic API Key\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edeepseek-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDeepSeek API Key\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ehuggingface-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHugging Face API Token\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"payments-and-commerce\"\u003ePayments and Commerce\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eDetects\u003c/th\u003e\n\u003cth\u003eSeverity\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003estripe-api-key-live\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eStripe Live API Key\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003estripe-api-key-test\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eStripe Test API Key\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecoinbase-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCoinbase API Key\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eshopify-access-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eShopify Access Token\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"dev-tools-ci-and-packages\"\u003eDev Tools, CI, and Packages\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eDetects\u003c/th\u003e\n\u003cth\u003eSeverity\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egithub-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGitHub Personal Access Token — both the classic \u003ccode\u003eghp_\u003c/code\u003e PAT and the fine-grained \u003ccode\u003egithub_pat_\u003c/code\u003e PAT\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egithub-oauth-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGitHub OAuth2 \u0026amp; installation token — \u003ccode\u003egho_\u003c/code\u003e/\u003ccode\u003eghu_\u003c/code\u003e/\u003ccode\u003eghr_\u003c/code\u003e/\u003ccode\u003eghs_\u003c/code\u003e, including new stateless (JWT-format) \u003ccode\u003eghs_\u003c/code\u003e installation tokens\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egitlab-pat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGitLab Personal Access Token — the classic \u003ccode\u003eglpat-\u003c/code\u003e token plus the newer routable prefixes: deploy (\u003ccode\u003egldt-\u003c/code\u003e), runner (\u003ccode\u003eglrt-\u003c/code\u003e), CI/CD build \u0026amp; trigger (\u003ccode\u003eglcbt-\u003c/code\u003e/\u003ccode\u003eglptt-\u003c/code\u003e), OAuth application secret (\u003ccode\u003egloas-\u003c/code\u003e), and feed (\u003ccode\u003eglft-\u003c/code\u003e) tokens; self-hosted GitLab hosts are detected for verification when present nearby\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ebitbucket-app-password\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBitbucket App Password\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecircleci-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCircleCI Personal API Token\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003enpm-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNPM Access Token\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epypi-api-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePyPI API Token\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003erubygems-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRubyGems API Key\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edockerhub-pat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDocker Hub Personal Access Token\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esonarcloud-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSonarCloud/SonarQube Token\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esnyk-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSnyk API Key\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edatabricks-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDatabricks Personal Access Token\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003elaunchdarkly-sdk-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eLaunchDarkly SDK Key\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"communication-and-collaboration\"\u003eCommunication and Collaboration\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eDetects\u003c/th\u003e\n\u003cth\u003eSeverity\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eslack-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSlack Bot/User Token\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eslack-webhook\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSlack Webhook URL\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eteams-webhook\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMicrosoft Teams Incoming Webhook URL\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ediscord-bot-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDiscord Bot Token\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ediscord-webhook-url\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDiscord Webhook URL\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003etelegram-bot-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTelegram Bot Token\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003enotion-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNotion Internal Integration Token\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003elinear-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eLinear API Key\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003efigma-pat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFigma Personal Access Token\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eairtable-pat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAirtable Personal Access Token\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"email-and-messaging-delivery\"\u003eEmail and Messaging Delivery\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eDetects\u003c/th\u003e\n\u003cth\u003eSeverity\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esendgrid-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSendGrid API Key\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003emailgun-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMailgun API Key\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epostmark-server-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePostmark Server API Token\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003etwilio-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTwilio API Key\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"monitoring-and-observability\"\u003eMonitoring and Observability\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eDetects\u003c/th\u003e\n\u003cth\u003eSeverity\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edatadog-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDatadog API Key\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003enewrelic-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNew Relic API Key\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egrafana-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGrafana API Key\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esentry-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSentry Auth Token\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epagerduty-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePagerDuty API Key\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"databases-and-connection-strings\"\u003eDatabases and Connection Strings\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eDetects\u003c/th\u003e\n\u003cth\u003eSeverity\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edatabase-connection-string\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDatabase Connection String\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eredis-connection-string\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRedis Connection String\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003erabbitmq-connection-string\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRabbitMQ Connection String\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esnowflake-credentials\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSnowflake Connection Credentials\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esupabase-service-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSupabase Service Role Key\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"identity-and-access\"\u003eIdentity and Access\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eDetects\u003c/th\u003e\n\u003cth\u003eSeverity\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eauth0-management-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAuth0 Management API Token\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eokta-api-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOkta API Token\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eldap-credentials\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eLDAP/LDAPS Bind Credentials\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"web3\"\u003eWeb3\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eDetects\u003c/th\u003e\n\u003cth\u003eSeverity\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003einfura-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInfura API Key\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"generic-and-cryptographic\"\u003eGeneric and Cryptographic\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eDetects\u003c/th\u003e\n\u003cth\u003eSeverity\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egeneric-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGeneric API Key\u003c/td\u003e\n\u003ctd\u003eMedium\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ejwt\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eJSON Web Token\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eprivate-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePrivate Key (RSA, SSH, DSA, EC, PGP), including PKCS8 \u003ccode\u003ePRIVATE KEY\u003c/code\u003e and password-protected \u003ccode\u003eENCRYPTED PRIVATE KEY\u003c/code\u003e armor\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eftp-credentials\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFTP/SFTP Credentials\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003chr\u003e\n\u003cp\u003e\u003cstrong\u003eTotal: 64 built-in detectors.\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"filtering-by-severity\"\u003eFiltering by severity\u003c/h2\u003e\n\u003cp\u003eFindings are filterable by severity using \u003ccode\u003e--min-severity\u003c/code\u003e at the command line or \u003ccode\u003eoutput.severity-threshold\u003c/code\u003e in config. Only findings at or above the specified level are included in the output. See \u003ca href=\"#/configuration/severity-and-filtering\"\u003eSeverity and Filtering\u003c/a\u003e for details.\u003c/p\u003e\n\u003ch2 id=\"excluding-specific-detectors\"\u003eExcluding specific detectors\u003c/h2\u003e\n\u003cp\u003eTo skip one or more detectors entirely, add their IDs to \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e in \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003efilter:\n exclude-detectors:\n - generic-api-key\n - jwt\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eSee \u003ca href=\"#/configuration/severity-and-filtering\"\u003eSeverity and Filtering\u003c/a\u003e for the full filtering reference.\u003c/p\u003e\n\u003ch2 id=\"verification-coverage\"\u003eVerification coverage\u003c/h2\u003e\n\u003cp\u003eSome detectors have a live verifier; others are format-validated only; ten have no verifier at all. See \u003ca href=\"#/verification/verification-coverage\"\u003eVerification Coverage\u003c/a\u003e for the complete breakdown.\u003c/p\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/detectors/custom-rules\"\u003eCustom Rules\u003c/a\u003e — define your own detection patterns in YAML.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/verification-coverage\"\u003eVerification Coverage\u003c/a\u003e — which detectors can be live-verified.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/severity-and-filtering\"\u003eSeverity and Filtering\u003c/a\u003e — filtering findings by severity or detector.\u003c/li\u003e\n\u003c/ul\u003e\n"},"getting-started/how-it-works":{"title":"How It Works","description":"Architecture of the Leakwatch scan pipeline: sources, detection, verification, and output.","html":"\u003ch1 id=\"how-it-works\"\u003eHow It Works\u003c/h1\u003e\n\u003cp\u003eUnderstanding the Leakwatch pipeline helps you tune performance, interpret results, and decide which flags to reach for. This page explains what happens from the moment you run a scan command to the moment a finding appears in your output.\u003c/p\u003e\n\u003ch2 id=\"the-pipeline-at-a-glance\"\u003eThe pipeline at a glance\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-mermaid\"\u003eflowchart LR\n A([Source\\nfs / git / image\\ns3 / gcs / slack]) --\u0026gt; B[Worker Pool\\n—concurrency workers]\n B --\u0026gt; C[Aho-Corasick\\nPre-filter]\n C --\u0026gt; D[Regex\\nDetectors]\n D --\u0026gt; E[Inline-ignore\\nCheck]\n E --\u0026gt; F[Verification\\nPool\\n4 workers / 10 rps]\n F --\u0026gt; G[Post-scan\\nFilters]\n G --\u0026gt; H([Output\\njson / sarif\\ncsv / table\\ngithub])\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eEach stage is described in detail below.\u003c/p\u003e\n\u003ch2 id=\"1-source\"\u003e1. Source\u003c/h2\u003e\n\u003cp\u003eEvery scan starts with a \u003cstrong\u003eSource\u003c/strong\u003e — an abstraction that emits chunks of data for the engine to process. Leakwatch ships six sources:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eSource\u003c/th\u003e\n\u003cth\u003eCommand\u003c/th\u003e\n\u003cth\u003eWhat it emits\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003eFilesystem\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003escan fs\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFile contents from a local directory tree\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eGit history\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003escan git\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eEvery blob across the full commit history\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eContainer image\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003escan image\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eLayer contents of an OCI/Docker image, daemonless\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eAWS S3\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003escan s3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eObject contents from an S3 bucket\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eGoogle Cloud Storage\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003escan gcs\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eObject contents from a GCS bucket\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eSlack\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003escan slack\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMessage text from channels and DMs\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNote\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eSlack scanning covers \u003cstrong\u003emessage text only\u003c/strong\u003e. The contents of files uploaded to Slack are not scanned.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003cp\u003eChunks flow into a buffered channel consumed by the worker pool.\u003c/p\u003e\n\u003ch2 id=\"2-worker-pool\"\u003e2. Worker pool\u003c/h2\u003e\n\u003cp\u003eThe engine maintains a fixed pool of \u003cstrong\u003egoroutines\u003c/strong\u003e — one per \u003ccode\u003e--concurrency\u003c/code\u003e value (default: number of CPUs). Each worker pulls a chunk from the channel and runs the detection pipeline independently. Because workers share no mutable state, the pool scales linearly with concurrency up to the limits of I/O and memory.\u003c/p\u003e\n\u003cp\u003eScans respond to \u003ccode\u003eSIGINT\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e: when a cancellation signal arrives, the context is cancelled, workers drain their current chunk and stop, and partial results are collected before output is written.\u003c/p\u003e\n\u003ch2 id=\"3-aho-corasick-keyword-pre-filter\"\u003e3. Aho-Corasick keyword pre-filter\u003c/h2\u003e\n\u003cp\u003eRunning 64 regex patterns on every chunk would be slow. Instead, the engine builds a single \u003cstrong\u003eAho-Corasick multi-pattern automaton\u003c/strong\u003e at startup from the keyword lists declared by each detector. For each chunk, this automaton does a single linear pass and returns only the detectors whose keywords appeared in the chunk's bytes.\u003c/p\u003e\n\u003cp\u003eThis means most detectors never run their regex on most chunks. Detectors that declare no keywords always run (they skip the pre-filter and proceed directly to regex).\u003c/p\u003e\n\u003cp\u003eThe Aho-Corasick implementation comes from \u003ca href=\"https://github.com/cloudflare/ahocorasick\"\u003ecloudflare/ahocorasick\u003c/a\u003e.\u003c/p\u003e\n\u003ch2 id=\"4-regex-detectors\"\u003e4. Regex detectors\u003c/h2\u003e\n\u003cp\u003eEach shortlisted detector runs its compiled \u003cstrong\u003eregular expression\u003c/strong\u003e against the chunk bytes. When a pattern matches, the detector returns a \u003ccode\u003eRawFinding\u003c/code\u003e containing:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eThe raw secret bytes (held in memory only for verification; never logged or written to disk).\u003c/li\u003e\n\u003cli\u003eA \u003cstrong\u003eredacted\u003c/strong\u003e representation safe for output.\u003c/li\u003e\n\u003cli\u003eOptional extra metadata (e.g. account ID for an AWS key).\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eLeakwatch ships \u003cstrong\u003e64 built-in detectors\u003c/strong\u003e across 60 packages, covering cloud providers, AI APIs, payment platforms, databases, messaging tools, version control, and more. You can add your own patterns via \u003ca href=\"#/detectors/custom-rules\"\u003ecustom YAML rules\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eAll detectors are registered at compile time using Go's \u003ccode\u003einit()\u003c/code\u003e function and blank imports (ADR-0004). There is no plugin loader or dynamic discovery at runtime.\u003c/p\u003e\n\u003ch2 id=\"5-inline-ignore-check\"\u003e5. Inline-ignore check\u003c/h2\u003e\n\u003cp\u003eBefore a finding is sent to verification, the engine checks whether the source line contains an \u003cstrong\u003einline ignore marker\u003c/strong\u003e:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-go\"\u003e// leakwatch:ignore\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eor a detector-scoped variant:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-go\"\u003e// leakwatch:ignore:aws-access-key-id\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eIf the marker is present, the finding is silently dropped \u003cstrong\u003ebefore any network call is made\u003c/strong\u003e. This is intentional: ignored secrets should never trigger a live API request.\u003c/p\u003e\n\u003ch2 id=\"6-verification\"\u003e6. Verification\u003c/h2\u003e\n\u003cp\u003eAfter detection completes for all chunks, the engine passes findings to a separate \u003cstrong\u003everification worker pool\u003c/strong\u003e (default 4 workers). Verification:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eIs guarded by a global \u003cstrong\u003erate limiter\u003c/strong\u003e (default 10 requests per second) shared across all workers.\u003c/li\u003e\n\u003cli\u003eApplies a \u003cstrong\u003eper-request timeout\u003c/strong\u003e (default 10 seconds) to every API call.\u003c/li\u003e\n\u003cli\u003eMakes only \u003cstrong\u003eread-only, non-destructive\u003c/strong\u003e calls to the provider (e.g. \u003ccode\u003ests:GetCallerIdentity\u003c/code\u003e for AWS keys).\u003c/li\u003e\n\u003cli\u003eMarks each finding with one of four statuses: \u003ccode\u003everified_active\u003c/code\u003e, \u003ccode\u003everified_inactive\u003c/code\u003e, \u003ccode\u003eunverified\u003c/code\u003e, or \u003ccode\u003everify_error\u003c/code\u003e.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eLeakwatch ships \u003cstrong\u003e54 verifiers\u003c/strong\u003e, covering 84.4% of the 64 built-in detector types. The remaining 10 types (such as JWTs and generic API keys) cannot be safely verified and are always reported as \u003ccode\u003eunverified\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003ePass \u003ccode\u003e--no-verify\u003c/code\u003e to skip this stage entirely — useful for fast, offline scans.\u003c/p\u003e\n\u003cp\u003eFor a deep dive into verification behavior and status meanings, see \u003ca href=\"#/verification/how-verification-works\"\u003eHow Verification Works\u003c/a\u003e.\u003c/p\u003e\n\u003ch2 id=\"7-finding-id-and-entropy\"\u003e7. Finding ID and entropy\u003c/h2\u003e\n\u003cp\u003eEach finding receives a \u003cstrong\u003edeterministic ID\u003c/strong\u003e computed as:\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003esha256(detectorID + redacted + filePath + line) → truncated to the first 16 bytes,\n hex-encoded to a 32-character string\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThe result is a flat 32-character lowercase hex string (e.g. \u003ccode\u003e447b5d2846d08ce25dd3d638cfe911ad\u003c/code\u003e) — \u003cstrong\u003enot\u003c/strong\u003e a dashed UUID. The same secret at the same location always produces the same ID, making it safe to deduplicate findings across runs or track them in issue trackers.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eShannon entropy\u003c/strong\u003e (range 0–8) is computed for each finding and exposed in output for informational purposes. At the engine level, the \u003ccode\u003edetection.entropy.threshold\u003c/code\u003e gate applies \u003cstrong\u003eonly\u003c/strong\u003e to heuristic detectors that explicitly opt in — currently just \u003ccode\u003egeneric-api-key\u003c/code\u003e — dropping a match whose entropy falls below the threshold to suppress low-randomness placeholders. Every structural (format-anchored) detector, such as \u003ccode\u003eaws-access-key-id\u003c/code\u003e or \u003ccode\u003egithub-token\u003c/code\u003e, is never gated by entropy: a low-entropy match from those detectors still appears in results. Custom rules apply their own independent per-rule \u003ccode\u003eentropy\u003c/code\u003e threshold (see \u003ca href=\"#/detectors/custom-rules\"\u003eCustom Rules\u003c/a\u003e), separate from this engine-level gate.\u003c/p\u003e\n\u003ch2 id=\"8-post-scan-filters\"\u003e8. Post-scan filters\u003c/h2\u003e\n\u003cp\u003eAfter verification, two filters apply:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e--only-verified\u003c/code\u003e — drops all findings that are not \u003ccode\u003everified_active\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e--min-severity\u003c/code\u003e — drops findings below the specified severity level (\u003ccode\u003elow\u003c/code\u003e | \u003ccode\u003emedium\u003c/code\u003e | \u003ccode\u003ehigh\u003c/code\u003e | \u003ccode\u003ecritical\u003c/code\u003e; default \u003ccode\u003elow\u003c/code\u003e).\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eBoth filters run after verification so that verification status is available when \u003ccode\u003e--only-verified\u003c/code\u003e is evaluated.\u003c/p\u003e\n\u003ch2 id=\"9-output\"\u003e9. Output\u003c/h2\u003e\n\u003cp\u003eSurviving findings are passed to one of five \u003cstrong\u003eformatters\u003c/strong\u003e:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFormat\u003c/th\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eCommon use\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003eJSON\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e--format json\u003c/code\u003e (default)\u003c/td\u003e\n\u003ctd\u003eMachine-readable, pipeline-friendly\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eSARIF v2.1.0\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e--format sarif\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGitHub Code Scanning, security dashboards\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eCSV\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e--format csv\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSpreadsheets, data analysis\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eTable\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e--format table\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTerminal review, color-coded by severity\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eGitHub annotations\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e--format github\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInline pull-request annotations in GitHub Actions\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eOutput goes to stdout by default; use \u003ccode\u003e--output \u0026lt;file\u0026gt;\u003c/code\u003e to write to a file.\u003c/p\u003e\n\u003cp\u003eA \u003cstrong\u003escan summary\u003c/strong\u003e (date, source type, target, files scanned, duration, findings count, interrupted status) is always printed to \u003cstrong\u003estderr\u003c/strong\u003e after every scan, regardless of format or output destination.\u003c/p\u003e\n\u003ch2 id=\"secret-safety\"\u003eSecret safety\u003c/h2\u003e\n\u003cp\u003eLeakwatch is designed so that discovered secrets never leave the process boundary except for verification calls:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eRaw secret bytes live only in memory during detection and verification.\u003c/li\u003e\n\u003cli\u003eThe \u003ccode\u003e--show-raw\u003c/code\u003e flag is \u003ccode\u003efalse\u003c/code\u003e by default; without it, only the redacted representation appears in output.\u003c/li\u003e\n\u003cli\u003eSecrets are never written to disk, logged via \u003ccode\u003eslog\u003c/code\u003e, or cached between runs.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"design-decisions\"\u003eDesign decisions\u003c/h2\u003e\n\u003cp\u003eThe architecture reflects several deliberate choices documented as ADRs:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eGo + CGO disabled\u003c/strong\u003e (ADR-0001) — single static binary, no runtime dependencies, cross-compiles to all platforms.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCobra + Viper\u003c/strong\u003e (ADR-0002) — hierarchical CLI with \u003ccode\u003eflag \u0026gt; env \u0026gt; config \u0026gt; default\u003c/code\u003e precedence.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ego-git\u003c/strong\u003e (ADR-0003) — pure Go Git library; no external \u003ccode\u003egit\u003c/code\u003e binary required.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCompile-time detector registration\u003c/strong\u003e (ADR-0004) — \u003ccode\u003einit()\u003c/code\u003e + blank imports; type-safe, no runtime plugin loader.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAho-Corasick hybrid matching\u003c/strong\u003e (ADR-0005) — pre-filter eliminates most regex work on irrelevant chunks.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ego-containerregistry\u003c/strong\u003e (ADR-0006) — daemonless layer analysis; no Docker daemon required to scan images.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eWorker pool\u003c/strong\u003e (ADR-0008) — fixed goroutine count, channel-based fan-out; predictable memory and CPU usage.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eQuick Start\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eHow Verification Works\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eConfiguration File\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Reference\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/detectors/custom-rules\"\u003eCustom Rules\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n"},"getting-started/installation":{"title":"Installation","description":"Install Leakwatch via Homebrew, go install, Docker, or a prebuilt binary.","html":"\u003ch1 id=\"installation\"\u003eInstallation\u003c/h1\u003e\n\u003cp\u003eGetting Leakwatch onto your machine takes less than a minute. Choose the method that best fits your workflow: Homebrew is the simplest option on macOS and Linux, \u003ccode\u003ego install\u003c/code\u003e is ideal if you already have a Go toolchain, Docker keeps your host system clean, and prebuilt binaries work everywhere without any toolchain at all.\u003c/p\u003e\n\u003ch2 id=\"homebrew-macos-and-linux\"\u003eHomebrew (macOS and Linux)\u003c/h2\u003e\n\u003cp\u003eThe official tap supports macOS and Linux on both amd64 and arm64.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003ebrew install HodeTech/tap/leakwatch\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThe tap is hosted at \u003ca href=\"https://github.com/HodeTech/homebrew-tap\"\u003egithub.com/HodeTech/homebrew-tap\u003c/a\u003e. Homebrew handles upgrades with \u003ccode\u003ebrew upgrade leakwatch\u003c/code\u003e.\u003c/p\u003e\n\u003ch2 id=\"go-install\"\u003eGo install\u003c/h2\u003e\n\u003cp\u003eIf you have Go 1.25 or later installed, you can build and install the latest release directly from source:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003ego install github.com/HodeTech/leakwatch@latest\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThe binary is placed in \u003ccode\u003e$(go env GOPATH)/bin\u003c/code\u003e. Make sure that directory is on your \u003ccode\u003ePATH\u003c/code\u003e.\u003c/p\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNote\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003e\u003ccode\u003ego install\u003c/code\u003e always fetches the latest tagged release. To pin a specific version, replace \u003ccode\u003e@latest\u003c/code\u003e with a tag such as \u003ccode\u003e@v1.5.0\u003c/code\u003e.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"docker\"\u003eDocker\u003c/h2\u003e\n\u003cp\u003eA minimal, multi-stage Alpine image is published to the GitHub Container Registry. The image runs as a non-root user (\u003ccode\u003eleakwatch\u003c/code\u003e), has CGO disabled, and uses \u003ccode\u003e/scan\u003c/code\u003e as its working directory.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003edocker run --rm \\\n -v \u0026quot;$(pwd):/scan\u0026quot; \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan fs /scan\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eAvailable tags:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eTag\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e:latest\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMost recent release\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e:v1.5.0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eExact version pin\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e:v1.5\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMinor-version pin (tracks patch releases)\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eMount the directory you want to scan to \u003ccode\u003e/scan\u003c/code\u003e inside the container. Flags and options work identically to the native binary — see \u003ca href=\"#/reference/cli-reference\"\u003eCLI Reference\u003c/a\u003e for the full list.\u003c/p\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eTip\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eFor Docker-specific usage patterns, including scanning remote Git repositories and passing credentials securely, see \u003ca href=\"#/ci-cd/docker-usage\"\u003eUsing Docker\u003c/a\u003e.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"prebuilt-binary\"\u003ePrebuilt binary\u003c/h2\u003e\n\u003cp\u003eEvery release publishes tarballs for all supported platforms on the \u003ca href=\"https://github.com/HodeTech/Leakwatch/releases\"\u003eGitHub Releases\u003c/a\u003e page. Download the archive for your platform, extract it, and place the binary on your \u003ccode\u003ePATH\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSupported platforms:\u003c/strong\u003e Linux, macOS, and Windows on amd64 and arm64.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Example for Linux amd64 — replace OS and ARCH to match your platform\ncurl -LO https://github.com/HodeTech/Leakwatch/releases/latest/download/leakwatch_Linux_amd64.tar.gz\ntar -xzf leakwatch_Linux_amd64.tar.gz\nsudo mv leakwatch /usr/local/bin/leakwatch\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003ePlatform naming follows the pattern \u003ccode\u003eleakwatch_\u0026lt;OS\u0026gt;_\u0026lt;ARCH\u0026gt;.tar.gz\u003c/code\u003e where \u003ccode\u003e\u0026lt;OS\u0026gt;\u003c/code\u003e is \u003ccode\u003eLinux\u003c/code\u003e, \u003ccode\u003eDarwin\u003c/code\u003e, or \u003ccode\u003eWindows\u003c/code\u003e and \u003ccode\u003e\u0026lt;ARCH\u0026gt;\u003c/code\u003e is \u003ccode\u003eamd64\u003c/code\u003e or \u003ccode\u003earm64\u003c/code\u003e.\u003c/p\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eTip\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eEvery release also publishes a Software Bill of Materials (SBOM) for each archive and a \u003ca href=\"https://github.com/sigstore/cosign\"\u003eSigstore/cosign\u003c/a\u003e keyless signature (\u003ccode\u003echecksums.txt.sig\u003c/code\u003e / \u003ccode\u003echecksums.txt.pem\u003c/code\u003e) over the release's checksums file, so the provenance of a downloaded binary can be verified in supply-chain-sensitive environments.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"verifying-your-installation\"\u003eVerifying your installation\u003c/h2\u003e\n\u003cp\u003eAfter any installation method, confirm the binary is reachable and check the version:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch version\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eExpected output:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eleakwatch v1.5.0 (commit: a3f9c12, built: 2026-05-10T08:22:00Z)\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eIf the command is not found, check that the install directory is on your \u003ccode\u003ePATH\u003c/code\u003e.\u003c/p\u003e\n\u003ch2 id=\"next-steps\"\u003eNext steps\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eQuick Start\u003c/a\u003e — run your first scan in under a minute.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/how-it-works\"\u003eHow It Works\u003c/a\u003e — the architecture behind a Leakwatch scan.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eConfiguration File\u003c/a\u003e — customize scan behavior with \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eQuick Start\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/docker-usage\"\u003eUsing Docker\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Reference\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eConfiguration File\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n"},"getting-started/introduction":{"title":"Introduction","description":"What Leakwatch is, what it scans, and how it detects and verifies leaked secrets.","html":"\u003ch1 id=\"introduction\"\u003eIntroduction\u003c/h1\u003e\n\u003cp\u003e\u003cstrong\u003eLeakwatch\u003c/strong\u003e is a high-performance, open-source (MIT) security tool that \u003cstrong\u003edetects, verifies, and reports leaked secrets\u003c/strong\u003e — API keys, tokens, passwords, connection strings, and private keys — across your codebases, Git history, container images, cloud storage, and Slack workspaces.\u003c/p\u003e\n\u003cp\u003eIt is written in Go, ships as a single static binary with no runtime dependencies (\u003ccode\u003eCGO_ENABLED=0\u003c/code\u003e), and is built to run anywhere: a developer laptop, a pre-commit hook, or a CI/CD pipeline.\u003c/p\u003e\n\u003ch2 id=\"why-leakwatch\"\u003eWhy Leakwatch\u003c/h2\u003e\n\u003cp\u003eA leaked credential in a single commit — even one later deleted — can stay reachable in Git history forever and be exploited within minutes of being pushed. Leakwatch is designed to catch those secrets early and tell you which ones are \u003cem\u003eactually dangerous\u003c/em\u003e:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eBroad detection\u003c/strong\u003e — 64 built-in detectors covering cloud providers, AI APIs, payment platforms, databases, messaging tools, and more, plus your own YAML custom rules.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eVerification, not just detection\u003c/strong\u003e — for 54 detector types Leakwatch can confirm whether a found secret is \u003cem\u003estill live\u003c/em\u003e by making a controlled, read-only call to the provider. A verified-active key is an incident; an inactive one is noise.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMany sources\u003c/strong\u003e — scan a local filesystem, a full Git history, an OCI/Docker image, AWS S3, Google Cloud Storage, and Slack messages.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCI-native output\u003c/strong\u003e — JSON, SARIF (for GitHub Code Scanning), CSV, a colorized terminal table, and inline GitHub Actions annotations.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSecret-safe by design\u003c/strong\u003e — discovered secrets are redacted by default and are never logged, cached, or written to disk.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"what-it-scans\"\u003eWhat it scans\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eSource\u003c/th\u003e\n\u003cth\u003eCommand\u003c/th\u003e\n\u003cth\u003eWhat it covers\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003eFilesystem\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eleakwatch scan fs\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFiles in a local directory tree\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eGit history\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eleakwatch scan git\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eEvery blob across the full commit history (local or remote)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eContainer image\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eleakwatch scan image\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOCI/Docker image layers, daemonless\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eAWS S3\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eleakwatch scan s3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eObjects in an S3 bucket\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eGoogle Cloud Storage\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eleakwatch scan gcs\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eObjects in a GCS bucket\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eSlack\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eleakwatch scan slack\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMessage text in channels and (optionally) DMs\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eMultiple repos\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eleakwatch scan repos\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSeveral Git repositories at once\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"how-detection-works-briefly\"\u003eHow detection works, briefly\u003c/h2\u003e\n\u003cp\u003eLeakwatch uses a layered pipeline so it stays fast even on large inputs:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eAho-Corasick keyword pre-filter\u003c/strong\u003e — a single multi-pattern automaton quickly decides which detectors \u003cem\u003ecould\u003c/em\u003e match a chunk, so most detectors never run their regex.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRegex validation\u003c/strong\u003e — only the shortlisted detectors run their precise patterns.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eEntropy\u003c/strong\u003e — Shannon entropy is computed for display, and gates the built-in \u003ccode\u003egeneric-api-key\u003c/code\u003e detector plus any custom rule with its own entropy threshold, dropping low-randomness placeholder matches. Every other (structural) built-in detector is never gated by entropy.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eVerification\u003c/strong\u003e — eligible findings are checked against the live provider API.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eTip\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eYou don't have to understand the pipeline to use Leakwatch — but it explains why scans are fast and why some findings show a verification status while others don't. See \u003ca href=\"#/getting-started/how-it-works\"\u003eHow It Works\u003c/a\u003e for the full picture.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"what-leakwatch-is-not\"\u003eWhat Leakwatch is \u003cem\u003enot\u003c/em\u003e\u003c/h2\u003e\n\u003cp\u003eTo set expectations accurately:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eIt does \u003cstrong\u003enot\u003c/strong\u003e rewrite Git history or remove secrets for you — it finds and reports them, and (with \u003ccode\u003e--remediation\u003c/code\u003e) tells you how to rotate them.\u003c/li\u003e\n\u003cli\u003eSlack scanning covers \u003cstrong\u003emessage text only\u003c/strong\u003e; scanning the \u003cem\u003econtents\u003c/em\u003e of uploaded files is not implemented.\u003c/li\u003e\n\u003cli\u003eVerification is available for many but not all secret types — 10 detector types (such as JWTs and generic API keys) cannot be safely verified and are always reported as unverified.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"next-steps\"\u003eNext steps\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/installation\"\u003eInstallation\u003c/a\u003e — install via Homebrew, \u003ccode\u003ego install\u003c/code\u003e, Docker, or a prebuilt binary.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eQuick Start\u003c/a\u003e — run your first scan in under a minute.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/how-it-works\"\u003eHow It Works\u003c/a\u003e — the architecture behind the scan.\u003c/li\u003e\n\u003c/ul\u003e\n"},"getting-started/quick-start":{"title":"Quick Start","description":"Run your first Leakwatch scan in under a minute.","html":"\u003ch1 id=\"quick-start\"\u003eQuick Start\u003c/h1\u003e\n\u003cp\u003eThe fastest way to understand what Leakwatch can do is to point it at a real directory. This page walks you through your first scan, explains what the output means, and shows the flags you will reach for most often.\u003c/p\u003e\n\u003ch2 id=\"prerequisites\"\u003ePrerequisites\u003c/h2\u003e\n\u003cp\u003eLeakwatch must be installed and accessible on your \u003ccode\u003ePATH\u003c/code\u003e. If you have not done that yet, see \u003ca href=\"#/getting-started/installation\"\u003eInstallation\u003c/a\u003e.\u003c/p\u003e\n\u003ch2 id=\"your-first-scan\"\u003eYour first scan\u003c/h2\u003e\n\u003cp\u003eScan the current directory with one command:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs .\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBy default, output is JSON written to stdout. To get a human-readable, colorized table instead, add \u003ccode\u003e--format table\u003c/code\u003e:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eHere is what a result looks like (the table formatter always prints a trailing REMEDIATION column; it shows \u003ccode\u003e-\u003c/code\u003e unless \u003ccode\u003e--remediation\u003c/code\u003e is also set):\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eSEVERITY DETECTOR FILE LINE REDACTED STATUS REMEDIATION\n-------- -------- ---- ---- -------- ------ -----------\nCRITICAL aws-access-key-id config/deploy.env 12 ****MNOP verified_active -\nHIGH npm-token scripts/bootstrap.sh 37 npm_****wxyz verified_active -\nMEDIUM generic-api-key src/services/analytics.js 89 ****w2y4 unverified -\n\nFound 3 secrets (1 critical, 1 high, 1 medium).\n\n── Scan Summary ─────────────────────────────────\n Date: 2026-05-23 14:03:11\n Source: filesystem\n Target: /home/user/myproject\n Files scanned: 312\n Duration: 1.24s\n Findings: 3\n─────────────────────────────────────────────────\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThe scan summary is always printed to \u003cstrong\u003estderr\u003c/strong\u003e, so it never interferes with piped or redirected output.\u003c/p\u003e\n\u003ch2 id=\"understanding-a-finding\"\u003eUnderstanding a finding\u003c/h2\u003e\n\u003cp\u003eEach row in the table (or object in JSON) represents one finding. The key fields are:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eField\u003c/th\u003e\n\u003cth\u003eMeaning\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eSEVERITY\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003eHow critical the secret type is: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e, or \u003ccode\u003ecritical\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eDETECTOR\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003eThe detector that matched — identifies the secret type (e.g. \u003ccode\u003eaws-access-key-id\u003c/code\u003e)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eFILE\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003ePath to the file where the secret was found, relative to the scan root\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eLINE\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003eLine number of the match\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eREDACTED\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003eA masked representation of the secret — never the raw value unless \u003ccode\u003e--show-raw\u003c/code\u003e is set\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eSTATUS\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003eVerification outcome: \u003ccode\u003everified_active\u003c/code\u003e, \u003ccode\u003everified_inactive\u003c/code\u003e, \u003ccode\u003eunverified\u003c/code\u003e, or \u003ccode\u003everify_error\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eREMEDIATION\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003eRotation/revocation guidance title, or \u003ccode\u003e-\u003c/code\u003e when \u003ccode\u003e--remediation\u003c/code\u003e was not passed\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eA \u003ccode\u003everified_active\u003c/code\u003e status means Leakwatch confirmed the secret is still live by making a read-only API call to the provider. \u003cstrong\u003eTreat every \u003ccode\u003everified_active\u003c/code\u003e finding as an open incident.\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"common-scan-options\"\u003eCommon scan options\u003c/h2\u003e\n\u003ch3 id=\"focus-on-confirmed-secrets-only\"\u003eFocus on confirmed secrets only\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --only-verified\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThis hides unverified and inactive findings, leaving only those confirmed live. Useful for triage when you have many results.\u003c/p\u003e\n\u003ch3 id=\"skip-network-verification-for-a-fast-offline-scan\"\u003eSkip network verification for a fast offline scan\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --no-verify\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eVerification is skipped entirely — no outbound network calls are made. Results appear faster and work without internet access, but all findings are marked \u003ccode\u003eunverified\u003c/code\u003e.\u003c/p\u003e\n\u003ch3 id=\"add-remediation-guidance\"\u003eAdd remediation guidance\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --remediation --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eEach finding gains a \u003cstrong\u003eREMEDIATION\u003c/strong\u003e column explaining how to rotate or revoke the specific secret type. The same data is included in JSON, SARIF, and CSV output when the flag is set.\u003c/p\u003e\n\u003ch3 id=\"filter-by-minimum-severity\"\u003eFilter by minimum severity\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --min-severity high\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eOnly findings at \u003ccode\u003ehigh\u003c/code\u003e or \u003ccode\u003ecritical\u003c/code\u003e severity are reported.\u003c/p\u003e\n\u003ch3 id=\"save-results-to-a-file\"\u003eSave results to a file\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --format sarif --output results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThe \u003ccode\u003e--output\u003c/code\u003e / \u003ccode\u003e-o\u003c/code\u003e flag writes to a file instead of stdout. SARIF output is compatible with \u003ca href=\"https://docs.github.com/en/code-security/code-scanning\"\u003eGitHub Code Scanning\u003c/a\u003e.\u003c/p\u003e\n\u003ch2 id=\"generate-a-configuration-file\"\u003eGenerate a configuration file\u003c/h2\u003e\n\u003cp\u003eRunning Leakwatch with defaults is fine for a first try, but for repeated use you will want a project-level configuration:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch init\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThis writes \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e in the current directory with recommended defaults for concurrency, entropy, verification, output format, and common path exclusions. Use \u003ccode\u003e--force\u003c/code\u003e to overwrite an existing file, or \u003ccode\u003e--output\u003c/code\u003e to write to a different path.\u003c/p\u003e\n\u003cp\u003eSee \u003ca href=\"#/configuration/config-file\"\u003eConfiguration File\u003c/a\u003e for a full explanation of every option.\u003c/p\u003e\n\u003ch2 id=\"exit-codes\"\u003eExit codes\u003c/h2\u003e\n\u003cp\u003eLeakwatch uses distinct exit codes so CI scripts can act on results without parsing output:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eCode\u003c/th\u003e\n\u003cth\u003eMeaning\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan completed — no findings\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan completed — one or more secrets found\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan failed due to an error\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan was interrupted (\u003ccode\u003eCtrl+C\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e) before it completed, and no findings had been reported yet\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eA typical CI gate looks like:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --only-verified --format sarif --output results.sarif\nif [ $? -eq 1 ]; then\n echo \u0026quot;Active secrets found — failing build\u0026quot;\n exit 1\nfi\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-warn\"\u003e\u003cdiv class=\"callout-label\"\u003eWarning\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eExit code \u003ccode\u003e1\u003c/code\u003e is returned whenever \u003cem\u003eany\u003c/em\u003e finding passes the active filters (including \u003ccode\u003e--min-severity\u003c/code\u003e and \u003ccode\u003e--only-verified\u003c/code\u003e). A clean exit code \u003ccode\u003e0\u003c/code\u003e means no findings matched — not that no secrets exist in the codebase.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"cancelling-a-scan\"\u003eCancelling a scan\u003c/h2\u003e\n\u003cp\u003ePress \u003ccode\u003eCtrl+C\u003c/code\u003e (or send \u003ccode\u003eSIGTERM\u003c/code\u003e) to cancel a running scan. Leakwatch stops gracefully: in-flight chunks finish, partial results are written, and the summary indicates \u003ccode\u003eStatus: interrupted (partial results)\u003c/code\u003e. Findings still take precedence over interruption — if secrets were already found before the interrupt, the scan exits \u003ccode\u003e1\u003c/code\u003e so that signal is never masked. Exit code \u003ccode\u003e3\u003c/code\u003e is returned only when the scan was interrupted with zero findings reported, so a CI pipeline can never mistake a scan that did not finish for a clean pass.\u003c/p\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/installation\"\u003eInstallation\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/how-it-works\"\u003eHow It Works\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Reference\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eConfiguration File\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eHow Verification Works\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n"},"output/output-formats":{"title":"Output Formats","description":"The five output formats Leakwatch supports — JSON, SARIF, CSV, table, and GitHub annotations — with examples and guidance on when to use each.","html":"\u003ch1 id=\"output-formats\"\u003eOutput Formats\u003c/h1\u003e\n\u003cp\u003eLeakwatch supports five output formats, covering machine-readable pipelines, security tooling integrations, spreadsheet exports, human-readable terminal review, and GitHub Actions annotations. Select a format with \u003ccode\u003e--format\u003c/code\u003e (or \u003ccode\u003e-f\u003c/code\u003e); write to a file instead of stdout with \u003ccode\u003e--output\u003c/code\u003e (or \u003ccode\u003e-o\u003c/code\u003e).\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --format json\nleakwatch scan fs . --format sarif --output results.sarif\nleakwatch scan fs . --format csv --output findings.csv\nleakwatch scan fs . --format table\nleakwatch scan fs . --format github # GitHub Actions annotations (CI use)\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThe default format is \u003ccode\u003ejson\u003c/code\u003e.\u003c/p\u003e\n\u003ch2 id=\"json\"\u003eJSON\u003c/h2\u003e\n\u003cp\u003eJSON is the default format and the most complete representation. Leakwatch writes a JSON \u003cstrong\u003earray\u003c/strong\u003e of finding objects to stdout (or to the file given by \u003ccode\u003e--output\u003c/code\u003e).\u003c/p\u003e\n\u003cp\u003eThe raw secret value is \u003cstrong\u003enever\u003c/strong\u003e serialized unless \u003ccode\u003e--show-raw\u003c/code\u003e is explicitly set. With \u003ccode\u003e--show-raw\u003c/code\u003e, a \u003ccode\u003e\u0026quot;raw\u0026quot;\u003c/code\u003e field is added to each object.\u003c/p\u003e\n\u003ch3 id=\"example-invocation\"\u003eExample invocation\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs ./src --format json --output findings.json\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"example-finding-object\"\u003eExample finding object\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-json\"\u003e{\n \u0026quot;id\u0026quot;: \u0026quot;447b5d2846d08ce25dd3d638cfe911ad\u0026quot;,\n \u0026quot;detector_id\u0026quot;: \u0026quot;github-token\u0026quot;,\n \u0026quot;severity\u0026quot;: \u0026quot;critical\u0026quot;,\n \u0026quot;redacted\u0026quot;: \u0026quot;ghp_****************************Xk9R\u0026quot;,\n \u0026quot;source\u0026quot;: {\n \u0026quot;source_type\u0026quot;: \u0026quot;filesystem\u0026quot;,\n \u0026quot;file_path\u0026quot;: \u0026quot;scripts/deploy.sh\u0026quot;,\n \u0026quot;line\u0026quot;: 14\n },\n \u0026quot;verification\u0026quot;: {\n \u0026quot;status\u0026quot;: \u0026quot;verified_active\u0026quot;\n },\n \u0026quot;entropy\u0026quot;: 5.82,\n \u0026quot;detected_at\u0026quot;: \u0026quot;2026-05-23T10:15:30Z\u0026quot;\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003eid\u003c/code\u003e is a deterministic, truncated SHA-256 rendered as a flat 32-character lowercase hex string — it is \u003cstrong\u003enot\u003c/strong\u003e a UUID and never contains dashes. See \u003ca href=\"#/getting-started/how-it-works\"\u003eHow It Works\u003c/a\u003e for how it is computed.\u003c/p\u003e\n\u003cp\u003eWhen \u003ccode\u003e--remediation\u003c/code\u003e is also set, a \u003ccode\u003e\u0026quot;remediation\u0026quot;\u003c/code\u003e object is nested inside each finding. See \u003ca href=\"#/output/remediation\"\u003eRemediation Guidance\u003c/a\u003e.\u003c/p\u003e\n\u003ch2 id=\"sarif\"\u003eSARIF\u003c/h2\u003e\n\u003cp\u003eThe \u003ccode\u003esarif\u003c/code\u003e format produces a SARIF v2.1.0 document, designed for upload to \u003ca href=\"https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/uploading-a-sarif-file-to-github\"\u003eGitHub Code Scanning\u003c/a\u003e. The tool name is \u003ccode\u003eLeakwatch\u003c/code\u003e and \u003ccode\u003einformationUri\u003c/code\u003e points to \u003ccode\u003ehttps://github.com/HodeTech/Leakwatch\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003eEach detector that appears in the findings becomes a \u003cstrong\u003erule\u003c/strong\u003e in the SARIF driver, complete with \u003ccode\u003ehelp\u003c/code\u003e text (populated from remediation steps when \u003ccode\u003e--remediation\u003c/code\u003e is set) and a \u003ccode\u003ehelpUri\u003c/code\u003e pointing to the provider documentation. Results carry a \u003ccode\u003eleakwatch/v1\u003c/code\u003e partial fingerprint computed from the detector ID, redacted value, and file path — this lets GitHub Code Scanning track the same alert even when surrounding code shifts.\u003c/p\u003e\n\u003ch3 id=\"example-invocation-1\"\u003eExample invocation\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --format sarif --output results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"uploading-to-github-code-scanning\"\u003eUploading to GitHub Code Scanning\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e# In a GitHub Actions workflow step:\n- name: Upload SARIF results\n uses: github/codeql-action/upload-sarif@v3\n with:\n sarif_file: results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eSee \u003ca href=\"#/ci-cd/github-action\"\u003eGitHub Action\u003c/a\u003e for the full CI setup.\u003c/p\u003e\n\u003ch2 id=\"csv\"\u003eCSV\u003c/h2\u003e\n\u003cp\u003eThe \u003ccode\u003ecsv\u003c/code\u003e format writes a header row followed by one row per finding, using standard comma-separated values. Every cell is sanitized against spreadsheet formula injection before writing.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eColumns (default):\u003c/strong\u003e\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eid,detector_id,severity,redacted,source,file_path,line,commit,verification_status,remediation\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003esource\u003c/code\u003e is a human-readable label identifying the origin repository, container image, or Slack channel a finding came from (useful when a \u003ccode\u003escan repos\u003c/code\u003e run combines findings from several repositories into one CSV); it is empty for a plain filesystem scan of a single directory. \u003ccode\u003eline\u003c/code\u003e is the line number, empty when not applicable.\u003c/p\u003e\n\u003cp\u003eWhen \u003ccode\u003e--show-raw\u003c/code\u003e is set, a trailing \u003ccode\u003eraw\u003c/code\u003e column is appended.\u003c/p\u003e\n\u003cp\u003eThe \u003ccode\u003eremediation\u003c/code\u003e column contains the remediation title (e.g. \u003ccode\u003e\u0026quot;Revoke GitHub Token\u0026quot;\u003c/code\u003e) when \u003ccode\u003e--remediation\u003c/code\u003e is set, and is empty otherwise.\u003c/p\u003e\n\u003ch3 id=\"example-invocation-2\"\u003eExample invocation\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git . --format csv --output findings.csv\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"example-output\"\u003eExample output\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-csv\"\u003eid,detector_id,severity,redacted,source,file_path,line,commit,verification_status,remediation\n447b5d2846d08ce25dd3d638cfe911ad,github-token,critical,ghp_****Xk9R,,scripts/deploy.sh,14,7d3e1f2,verified_active,Revoke GitHub Token\ne6fa909746d7d5242309b64c33209fa9,aws-access-key-id,high,AKIA****K7NP,,config/aws.yml,3,7d3e1f2,unverified,Rotate AWS Access Key\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"table\"\u003eTable\u003c/h2\u003e\n\u003cp\u003eThe \u003ccode\u003etable\u003c/code\u003e format writes a human-readable tab-aligned table, best suited for interactive terminal sessions where you want a quick visual scan of the results.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eColumns:\u003c/strong\u003e\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eSEVERITY | DETECTOR | FILE | LINE | REDACTED | STATUS | REMEDIATION\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThe \u003ccode\u003eLINE\u003c/code\u003e column shows \u003ccode\u003e-\u003c/code\u003e when no line number is available (for example a Slack or container-image finding). The \u003ccode\u003eREMEDIATION\u003c/code\u003e column is always present and shows \u003ccode\u003e-\u003c/code\u003e unless \u003ccode\u003e--remediation\u003c/code\u003e is set. When \u003ccode\u003e--show-raw\u003c/code\u003e is also set, a trailing \u003ccode\u003eRAW\u003c/code\u003e column is appended. A summary line is printed at the bottom of the table (e.g. \u003ccode\u003eFound 3 secrets (1 critical, 2 high).\u003c/code\u003e).\u003c/p\u003e\n\u003cp\u003eAttacker-influenced fields (detector ID, file path, redacted value) are stripped of control characters and ANSI escape sequences before being written to the terminal, so a maliciously crafted file name or custom-rule match cannot inject escape sequences into your terminal session.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eANSI color\u003c/strong\u003e is applied to the \u003ccode\u003eSEVERITY\u003c/code\u003e column automatically, but only when all four conditions are met:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003ccode\u003e--format table\u003c/code\u003e is selected\u003c/li\u003e\n\u003cli\u003eOutput goes to stdout (no \u003ccode\u003e--output \u0026lt;file\u0026gt;\u003c/code\u003e)\u003c/li\u003e\n\u003cli\u003estdout is a TTY (not a pipe or redirect)\u003c/li\u003e\n\u003cli\u003eThe \u003ccode\u003eNO_COLOR\u003c/code\u003e environment variable is unset\u003c/li\u003e\n\u003c/ol\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eSeverity\u003c/th\u003e\n\u003cth\u003eColor\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecritical\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBold red\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ehigh\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRed\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003emedium\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYellow\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBlue\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"example-invocation-3\"\u003eExample invocation\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --format table --min-severity high\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"example-output-1\"\u003eExample output\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eSEVERITY DETECTOR FILE LINE REDACTED STATUS REMEDIATION\n-------- -------- ---- ---- -------- ------ -----------\nCRITICAL github-token scripts/deploy.sh 14 ghp_****Xk9R verified_active Revoke GitHub Token\nHIGH aws-access-key-id config/aws.yml 3 AKIA****K7NP unverified Rotate AWS Access Key\n\nFound 2 secrets (1 critical, 1 high).\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"github-annotations\"\u003eGitHub annotations\u003c/h2\u003e\n\u003cp\u003eThe \u003ccode\u003egithub\u003c/code\u003e format emits \u003ca href=\"https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions\"\u003eGitHub Actions workflow commands\u003c/a\u003e (\u003ccode\u003e::error\u003c/code\u003e / \u003ccode\u003e::warning\u003c/code\u003e / \u003ccode\u003e::notice\u003c/code\u003e) so findings appear as \u003cstrong\u003einline annotations\u003c/strong\u003e on a pull request's \u003cem\u003eFiles changed\u003c/em\u003e view and in the run log. It is intended to be streamed to the runner's stdout — writing it to a file has no effect.\u003c/p\u003e\n\u003cp\u003eSeverity maps to the annotation level: \u003ccode\u003ecritical\u003c/code\u003e → \u003ccode\u003eerror\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e → \u003ccode\u003ewarning\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e/\u003ccode\u003elow\u003c/code\u003e → \u003ccode\u003enotice\u003c/code\u003e. A finding with a file path is anchored to that file and line; a finding without one becomes a run-level annotation.\u003c/p\u003e\n\u003cp\u003eFor safety, this format \u003cstrong\u003enever\u003c/strong\u003e prints the raw secret — only the redacted value is shown, even with \u003ccode\u003e--show-raw\u003c/code\u003e, because annotations render in the (often public) PR UI and logs.\u003c/p\u003e\n\u003ch3 id=\"example-invocation-4\"\u003eExample invocation\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --format github\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"example-output-2\"\u003eExample output\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003e::error file=config/prod.env,line=12,title=Leakwatch%3A aws-access-key-id::Potential secret detected by aws-access-key-id (critical): AKIA****K7NP\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThis format is normally driven by the \u003ca href=\"#/ci-cd/github-action\"\u003eGitHub Action\u003c/a\u003e (\u003ccode\u003eformat: github\u003c/code\u003e) rather than invoked by hand.\u003c/p\u003e\n\u003ch2 id=\"common-output-flags\"\u003eCommon output flags\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eShort\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-f\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOutput format: \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, \u003ccode\u003egithub\u003c/code\u003e (default \u003ccode\u003ejson\u003c/code\u003e)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-o\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eWrite to file instead of stdout\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--show-raw\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003c/td\u003e\n\u003ctd\u003eInclude unredacted secret value in output\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003c/td\u003e\n\u003ctd\u003eDrop findings below this severity level\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003c/td\u003e\n\u003ctd\u003eKeep only \u003ccode\u003everified_active\u003c/code\u003e findings\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--remediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003c/td\u003e\n\u003ctd\u003eEnrich findings with provider remediation guidance\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/output/remediation\"\u003eRemediation Guidance\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/github-action\"\u003eGitHub Action\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eHow Verification Works\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n"},"output/remediation":{"title":"Remediation Guidance","description":"Use --remediation to enrich findings with provider-specific rotation and revocation steps, urgency ratings, and official documentation links.","html":"\u003ch1 id=\"remediation-guidance\"\u003eRemediation Guidance\u003c/h1\u003e\n\u003cp\u003eKnowing a secret is leaked is only half the work — you also need to know what to do about it. Passing \u003ccode\u003e--remediation\u003c/code\u003e to any scan command enriches each finding with structured, provider-specific guidance: the steps to rotate or revoke the credential, a link to the provider's documentation, a link to the management console, an urgency rating, and a verification checklist.\u003c/p\u003e\n\u003ch2 id=\"how-to-enable-it\"\u003eHow to enable it\u003c/h2\u003e\n\u003cp\u003eAdd \u003ccode\u003e--remediation\u003c/code\u003e to any scan command:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --remediation\nleakwatch scan git . --remediation --format json\nleakwatch scan image myapp:latest --remediation --format sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eRemediation enrichment is disabled by default. When the flag is absent, the \u003ccode\u003eremediation\u003c/code\u003e field in each finding is \u003ccode\u003enull\u003c/code\u003e and no extra data is fetched or computed.\u003c/p\u003e\n\u003ch2 id=\"what-it-contains\"\u003eWhat it contains\u003c/h2\u003e\n\u003cp\u003eEach remediation entry includes the following fields:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eField\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003etitle\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eShort name of the remediation action (e.g. \u003ccode\u003e\u0026quot;Rotate AWS Access Key\u0026quot;\u003c/code\u003e)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esteps\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOrdered list of steps to rotate or revoke the secret\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edoc_url\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eLink to the provider's official credential-management documentation\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003econsole_url\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDirect link to the provider's management console page\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eurgency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHow quickly to act: \u003ccode\u003e\u0026quot;immediate\u0026quot;\u003c/code\u003e, \u003ccode\u003e\u0026quot;high\u0026quot;\u003c/code\u003e, or \u003ccode\u003e\u0026quot;medium\u0026quot;\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003echecklist\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePost-rotation verification steps (e.g. review audit logs, notify the security team)\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eLeakwatch ships 64 remediation entries — one for every built-in detector. All 64 entries are included in the binary; no network calls are made to fetch guidance.\u003c/p\u003e\n\u003ch2 id=\"how-it-appears-in-each-format\"\u003eHow it appears in each format\u003c/h2\u003e\n\u003cp\u003eEnrichment adds the guidance to the finding object in memory. How it surfaces depends on the output format:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eJSON\u003c/strong\u003e — the full structured \u003ccode\u003eremediation\u003c/code\u003e object is nested inside each finding:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --remediation --format json\n\u003c/code\u003e\u003c/pre\u003e\n\u003cpre\u003e\u003ccode class=\"language-json\"\u003e{\n \u0026quot;id\u0026quot;: \u0026quot;447b5d2846d08ce25dd3d638cfe911ad\u0026quot;,\n \u0026quot;detector_id\u0026quot;: \u0026quot;github-token\u0026quot;,\n \u0026quot;severity\u0026quot;: \u0026quot;critical\u0026quot;,\n \u0026quot;redacted\u0026quot;: \u0026quot;ghp_****************************Xk9R\u0026quot;,\n \u0026quot;source\u0026quot;: {\n \u0026quot;source_type\u0026quot;: \u0026quot;filesystem\u0026quot;,\n \u0026quot;file_path\u0026quot;: \u0026quot;scripts/deploy.sh\u0026quot;,\n \u0026quot;line\u0026quot;: 14\n },\n \u0026quot;verification\u0026quot;: {\n \u0026quot;status\u0026quot;: \u0026quot;verified_active\u0026quot;\n },\n \u0026quot;remediation\u0026quot;: {\n \u0026quot;title\u0026quot;: \u0026quot;Revoke GitHub Token\u0026quot;,\n \u0026quot;steps\u0026quot;: [\n \u0026quot;Go to GitHub Settings \u0026gt; Developer settings \u0026gt; Personal access tokens.\u0026quot;,\n \u0026quot;Revoke the compromised token immediately.\u0026quot;,\n \u0026quot;Create a new token with the minimum required scopes.\u0026quot;,\n \u0026quot;Update all integrations and CI/CD pipelines with the new token.\u0026quot;\n ],\n \u0026quot;doc_url\u0026quot;: \u0026quot;https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens\u0026quot;,\n \u0026quot;console_url\u0026quot;: \u0026quot;https://github.com/settings/tokens\u0026quot;,\n \u0026quot;urgency\u0026quot;: \u0026quot;immediate\u0026quot;,\n \u0026quot;checklist\u0026quot;: [\n \u0026quot;Review the GitHub audit log for unauthorized actions performed with the token.\u0026quot;,\n \u0026quot;Check repository and organization settings for unexpected changes.\u0026quot;,\n \u0026quot;Notify the security team about the exposure.\u0026quot;,\n \u0026quot;Scan for other repositories that may contain the same token.\u0026quot;\n ]\n },\n \u0026quot;entropy\u0026quot;: 5.82,\n \u0026quot;detected_at\u0026quot;: \u0026quot;2026-05-23T10:15:30Z\u0026quot;\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003cstrong\u003eSARIF\u003c/strong\u003e — the \u003ccode\u003esteps\u003c/code\u003e are embedded in the rule's \u003ccode\u003ehelp.text\u003c/code\u003e field, and \u003ccode\u003edoc_url\u003c/code\u003e is set as the rule's \u003ccode\u003ehelpUri\u003c/code\u003e. This surfaces directly in GitHub Code Scanning's alert details panel.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eCSV\u003c/strong\u003e — only the remediation \u003ccode\u003etitle\u003c/code\u003e is written to the \u003ccode\u003eremediation\u003c/code\u003e column. The full structured guidance is not included in the CSV output.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTable\u003c/strong\u003e — only the remediation \u003ccode\u003etitle\u003c/code\u003e is shown in the \u003ccode\u003eREMEDIATION\u003c/code\u003e column.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --remediation --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eSEVERITY DETECTOR FILE LINE REDACTED STATUS REMEDIATION\n-------- -------- ---- ---- -------- ------ -----------\nCRITICAL github-token scripts/deploy.sh 14 ghp_****Xk9R verified_active Revoke GitHub Token\n\nFound 1 secret (1 critical).\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eTip\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eUse \u003ccode\u003e--remediation --format json\u003c/code\u003e when you need the full structured guidance for automated incident-response workflows. Use \u003ccode\u003e--remediation --format table\u003c/code\u003e for a quick human-readable triage session in the terminal.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNote\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eEnrichment runs only when \u003ccode\u003e--remediation\u003c/code\u003e is set. Without the flag, the \u003ccode\u003eremediation\u003c/code\u003e field is absent from JSON and SARIF output, the CSV \u003ccode\u003eremediation\u003c/code\u003e column is empty, and the table \u003ccode\u003eREMEDIATION\u003c/code\u003e column shows \u003ccode\u003e-\u003c/code\u003e. The flag does not modify the original scan results — it adds a layer on top.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/output/output-formats\"\u003eOutput Formats\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eHow Verification Works\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n"},"reference/cli-reference":{"title":"CLI Reference","description":"Complete reference for every Leakwatch command, subcommand, and flag.","html":"\u003ch1 id=\"cli-reference\"\u003eCLI Reference\u003c/h1\u003e\n\u003cp\u003eThis page is the authoritative reference for all Leakwatch commands and flags. For conceptual explanations and worked examples, follow the cross-links to the relevant scanning or configuration pages.\u003c/p\u003e\n\u003ch2 id=\"global-flags\"\u003eGlobal flags\u003c/h2\u003e\n\u003cp\u003eThese flags are available on every command and subcommand.\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--config \u0026lt;path\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eauto-discovered \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePath to a configuration file. When omitted, Leakwatch searches the current directory, then the user's home directory, for \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e — there is \u003cstrong\u003eno\u003c/strong\u003e parent-directory walk. A malformed config file (invalid YAML) at either location is a fatal error.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--log-level \u0026lt;level\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ewarn\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eLogging verbosity: \u003ccode\u003edebug\u003c/code\u003e, \u003ccode\u003einfo\u003c/code\u003e, \u003ccode\u003ewarn\u003c/code\u003e, or \u003ccode\u003eerror\u003c/code\u003e. Log output goes to stderr and does not affect scan results.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"leakwatch-version\"\u003e\u003ccode\u003eleakwatch version\u003c/code\u003e\u003c/h2\u003e\n\u003cp\u003ePrints the binary version, commit hash, and build timestamp, then exits.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch version\n\u003c/code\u003e\u003c/pre\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eleakwatch v1.5.0 (commit: a3f9c12, built: 2026-05-10T08:22:00Z)\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"leakwatch-init\"\u003e\u003ccode\u003eleakwatch init\u003c/code\u003e\u003c/h2\u003e\n\u003cp\u003eGenerates a \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e configuration file in the current directory with recommended defaults.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch init [flags]\n\u003c/code\u003e\u003c/pre\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output \u0026lt;path\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e.leakwatch.yaml\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eWrite the config file to this path instead of the default.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--force\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOverwrite an existing config file. Without this flag, \u003ccode\u003einit\u003c/code\u003e exits with an error if the output file already exists.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Generate the default config\nleakwatch init\n\n# Overwrite an existing config\nleakwatch init --force\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"leakwatch-scan\"\u003e\u003ccode\u003eleakwatch scan\u003c/code\u003e\u003c/h2\u003e\n\u003cp\u003eParent command for all scan subcommands. Has no behavior on its own; run a subcommand.\u003c/p\u003e\n\u003ch3 id=\"common-scan-flags\"\u003eCommon scan flags\u003c/h3\u003e\n\u003cp\u003eThe following flags are available on \u003cstrong\u003eall\u003c/strong\u003e \u003ccode\u003escan\u003c/code\u003e subcommands.\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eShort\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-f\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ejson\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOutput format: \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, or \u003ccode\u003egithub\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-o\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estdout\u003c/td\u003e\n\u003ctd\u003eWrite results to this file path instead of stdout. A bare path with no extension is auto-suffixed with the format's own extension (e.g. \u003ccode\u003e--format sarif --output results\u003c/code\u003e writes \u003ccode\u003eresults.sarif\u003c/code\u003e). The file is written with \u003ccode\u003e0600\u003c/code\u003e permissions. Ignored for \u003ccode\u003e--format github\u003c/code\u003e, which always writes to stdout.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-c\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCPU count\u003c/td\u003e\n\u003ctd\u003eNumber of concurrent scan workers.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--max-file-size\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e10485760\u003c/code\u003e (10 MB)\u003c/td\u003e\n\u003ctd\u003eSkip files or blobs larger than this number of bytes.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--show-raw\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInclude the raw (unredacted) secret value in output. Use with caution.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-detectors\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eDetector IDs to exclude for this run (e.g. \u003ccode\u003eaws-access-key-id,generic-api-key\u003c/code\u003e). Repeatable or comma-separated; combined with (not a replacement for) \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e in the config file.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--no-verify\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDisable live secret verification. No outbound API calls are made.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eReport only findings that Leakwatch has confirmed are active via live verification.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMinimum severity to include in output: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e, or \u003ccode\u003ecritical\u003c/code\u003e. An unrecognized value (e.g. a typo) is a hard error rather than being silently accepted.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--remediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAttach remediation guidance (rotation/revocation steps) to each finding.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e (repeatable) is also available on every scan subcommand \u003cstrong\u003eexcept\u003c/strong\u003e \u003ccode\u003escan slack\u003c/code\u003e — see each subcommand's own flag table below, since it is registered per-command rather than shared by every subcommand.\u003c/p\u003e\n\u003cp\u003eSee \u003ca href=\"#/reference/exit-codes\"\u003eExit Codes\u003c/a\u003e for the full exit-code contract: \u003ccode\u003e0\u003c/code\u003e clean, \u003ccode\u003e1\u003c/code\u003e findings, \u003ccode\u003e2\u003c/code\u003e error, \u003ccode\u003e3\u003c/code\u003e interrupted before completion.\u003c/p\u003e\n\u003chr\u003e\n\u003ch3 id=\"scan-fs\"\u003e\u003ccode\u003escan fs\u003c/code\u003e\u003c/h3\u003e\n\u003cp\u003eScans one or more local filesystem paths — each may be a directory (walked recursively) or a single file.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs [path...] [flags]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eAccepts \u003cstrong\u003ezero or more\u003c/strong\u003e positional arguments. With no path given, \u003ccode\u003e.\u003c/code\u003e (the current directory) is scanned. Multiple paths may be mixed files and directories in one invocation.\u003c/p\u003e\n\u003ch4 id=\"filesystem-specific-flags\"\u003eFilesystem-specific flags\u003c/h4\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eGlob pattern for paths to exclude. Repeatable.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch4 id=\"examples\"\u003eExamples\u003c/h4\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Scan the current directory, print a colorized table\nleakwatch scan fs . --format table\n\n# Scan a single file\nleakwatch scan fs cmd/main.go\n\n# Scan several files and directories at once\nleakwatch scan fs cmd/ main.go internal/config\n\n# Save SARIF output, exclude test files and vendor\nleakwatch scan fs . \\\n --exclude \u0026quot;**/*_test.go\u0026quot; \\\n --exclude \u0026quot;vendor/**\u0026quot; \\\n --format sarif \\\n --output results.sarif\n\n# Exclude specific detectors for this run\nleakwatch scan fs . --exclude-detectors aws-access-key-id,generic-api-key\n\u003c/code\u003e\u003c/pre\u003e\n\u003chr\u003e\n\u003ch3 id=\"scan-git\"\u003e\u003ccode\u003escan git\u003c/code\u003e\u003c/h3\u003e\n\u003cp\u003eScans the full commit history of a local or remote Git repository.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git \u0026lt;url_or_path\u0026gt; [flags]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eExactly one positional argument is required: a local path or an HTTP/HTTPS/SSH URL.\u003c/p\u003e\n\u003ch4 id=\"git-specific-flags\"\u003eGit-specific flags\u003c/h4\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--since \u0026lt;YYYY-MM-DD\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eScan only commits after this date.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--since-commit \u0026lt;hash\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eScan only changes from this commit hash to HEAD.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--branch \u0026lt;name\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eTarget a specific branch instead of the default branch.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--depth \u0026lt;int\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e (full)\u003c/td\u003e\n\u003ctd\u003eShallow clone depth for remote repositories. \u003ccode\u003e0\u003c/code\u003e fetches the full history.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eGlob pattern for blob paths to exclude. Repeatable.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch4 id=\"examples-1\"\u003eExamples\u003c/h4\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Scan full local history\nleakwatch scan git . --format table\n\n# Scan only commits added by a pull request\nleakwatch scan git . --since-commit a1b2c3d --format json\n\u003c/code\u003e\u003c/pre\u003e\n\u003chr\u003e\n\u003ch3 id=\"scan-image\"\u003e\u003ccode\u003escan image\u003c/code\u003e\u003c/h3\u003e\n\u003cp\u003eScans the layers of an OCI/Docker image for secrets. Leakwatch is daemonless and pulls directly from the registry — no Docker socket is required.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan image \u0026lt;image:tag\u0026gt; [flags]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eExactly one positional argument is required. There are no image-specific flags beyond the common scan flags, which include \u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e (repeatable, matched against file paths inside layers).\u003c/p\u003e\n\u003ch4 id=\"examples-2\"\u003eExamples\u003c/h4\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Scan a public image\nleakwatch scan image nginx:latest --format table\n\n# Scan a private registry image and save JSON output\nleakwatch scan image registry.example.com/my-app:v2.3.0 \\\n --format json \\\n --output image-results.json\n\u003c/code\u003e\u003c/pre\u003e\n\u003chr\u003e\n\u003ch3 id=\"scan-s3\"\u003e\u003ccode\u003escan s3\u003c/code\u003e\u003c/h3\u003e\n\u003cp\u003eScans objects in an AWS S3 bucket.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan s3 \u0026lt;bucket\u0026gt; [flags]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eExactly one positional argument is required.\u003c/p\u003e\n\u003ch4 id=\"s3-specific-flags\"\u003eS3-specific flags\u003c/h4\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--prefix \u0026lt;string\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eLimit the scan to objects whose key starts with this prefix.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--region \u0026lt;string\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eAWS region of the bucket. Falls back to \u003ccode\u003eAWS_REGION\u003c/code\u003e environment variable or the AWS SDK default.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eGlob pattern for object keys to exclude. Repeatable.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch4 id=\"examples-3\"\u003eExamples\u003c/h4\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Scan an entire bucket\nleakwatch scan s3 my-data-bucket --region us-east-1 --format table\n\n# Scan only a specific prefix\nleakwatch scan s3 my-data-bucket --prefix backups/2026/ --format json\n\u003c/code\u003e\u003c/pre\u003e\n\u003chr\u003e\n\u003ch3 id=\"scan-gcs\"\u003e\u003ccode\u003escan gcs\u003c/code\u003e\u003c/h3\u003e\n\u003cp\u003eScans objects in a Google Cloud Storage bucket.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan gcs \u0026lt;bucket\u0026gt; [flags]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eExactly one positional argument is required.\u003c/p\u003e\n\u003ch4 id=\"gcs-specific-flags\"\u003eGCS-specific flags\u003c/h4\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--prefix \u0026lt;string\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eLimit the scan to objects whose name starts with this prefix.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--project \u0026lt;string\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eGCP project ID. Required when the bucket's project cannot be inferred from the default credentials.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eGlob pattern for object names to exclude. Repeatable.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch4 id=\"examples-4\"\u003eExamples\u003c/h4\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Scan an entire GCS bucket\nleakwatch scan gcs my-gcs-bucket --project my-gcp-project --format table\n\n# Scan a prefix\nleakwatch scan gcs my-gcs-bucket --prefix uploads/2026/ --format json\n\u003c/code\u003e\u003c/pre\u003e\n\u003chr\u003e\n\u003ch3 id=\"scan-slack\"\u003e\u003ccode\u003escan slack\u003c/code\u003e\u003c/h3\u003e\n\u003cp\u003eScans message text in a Slack workspace.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan slack [flags]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eNo positional arguments.\u003c/p\u003e\n\u003ch4 id=\"slack-specific-flags\"\u003eSlack-specific flags\u003c/h4\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--token \u0026lt;string\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eSlack bot token. Can also be set via \u003ccode\u003eLEAKWATCH_SLACK_TOKEN\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--channels \u0026lt;list\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eComma-separated list of channel names or IDs to scan. Scans all accessible channels when omitted.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-channels \u0026lt;list\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eComma-separated list of channel names or IDs to skip.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--since \u0026lt;YYYY-MM-DD\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eScan only messages posted after this date.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--include-dms\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInclude direct messages (requires additional OAuth scopes).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--rate-limit \u0026lt;float\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMaximum Slack API requests per second.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003ccode\u003escan slack\u003c/code\u003e has no \u003ccode\u003e--exclude\u003c/code\u003e path-pattern flag (unlike every other scan subcommand) — use \u003ccode\u003e--exclude-channels\u003c/code\u003e to skip whole channels instead.\u003c/p\u003e\n\u003ch4 id=\"examples-5\"\u003eExamples\u003c/h4\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Scan all accessible channels\nleakwatch scan slack --token xoxb-••••••••••••-••••••••••••-•••••••••••••••••••••••• --format table\n\n# Scan specific channels since a date\nleakwatch scan slack \\\n --token xoxb-••••••••••••-••••••••••••-••••••••••••••••••••••••• \\\n --channels general,engineering \\\n --since 2026-01-01 \\\n --format json\n\u003c/code\u003e\u003c/pre\u003e\n\u003chr\u003e\n\u003ch3 id=\"scan-repos\"\u003e\u003ccode\u003escan repos\u003c/code\u003e\u003c/h3\u003e\n\u003cp\u003eScans multiple Git repositories in parallel.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan repos \u0026lt;url_or_path...\u0026gt; [flags]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eRequires at least two positional arguments (repository URLs or local paths).\u003c/p\u003e\n\u003ch4 id=\"repos-specific-flags\"\u003eRepos-specific flags\u003c/h4\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eShort\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--parallel\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNumber of repositories to scan concurrently.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-c\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCPU count\u003c/td\u003e\n\u003ctd\u003eWorker concurrency within each repository scan.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eGlob pattern for blob paths to exclude, applied to every repository. Repeatable.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch4 id=\"examples-6\"\u003eExamples\u003c/h4\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Scan two repositories in parallel\nleakwatch scan repos \\\n https://github.com/org/repo-a.git \\\n https://github.com/org/repo-b.git \\\n --format json\n\n# Increase parallelism for a large set of repos\nleakwatch scan repos \\\n https://github.com/org/repo-a.git \\\n https://github.com/org/repo-b.git \\\n https://github.com/org/repo-c.git \\\n --parallel 3 \\\n --format sarif \\\n --output multi-repo.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003chr\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/reference/exit-codes\"\u003eExit Codes\u003c/a\u003e — how exit codes map to scan outcomes.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/environment-variables\"\u003eEnvironment Variables\u003c/a\u003e — configure Leakwatch without flags.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/filesystem\"\u003eFilesystem Scanning\u003c/a\u003e — detailed \u003ccode\u003escan fs\u003c/code\u003e guide.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/git-history\"\u003eGit History\u003c/a\u003e — detailed \u003ccode\u003escan git\u003c/code\u003e guide.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eConfiguration File\u003c/a\u003e — \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e reference.\u003c/li\u003e\n\u003c/ul\u003e\n"},"reference/environment-variables":{"title":"Environment Variables","description":"Environment variables that configure Leakwatch behavior without flags.","html":"\u003ch1 id=\"environment-variables\"\u003eEnvironment Variables\u003c/h1\u003e\n\u003cp\u003eLeakwatch reads configuration from three sources in priority order: \u003cstrong\u003ecommand-line flags\u003c/strong\u003e override \u003cstrong\u003eenvironment variables\u003c/strong\u003e, which override the \u003cstrong\u003econfig file\u003c/strong\u003e (\u003ccode\u003e.leakwatch.yaml\u003c/code\u003e), which falls back to built-in \u003cstrong\u003edefaults\u003c/strong\u003e. Environment variables are useful in CI environments where you cannot modify a config file or pass flags to every invocation.\u003c/p\u003e\n\u003ch2 id=\"configuration-variable-pattern\"\u003eConfiguration variable pattern\u003c/h2\u003e\n\u003cp\u003eAny key from \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e can be set as an environment variable by:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eUppercasing the key name.\u003c/li\u003e\n\u003cli\u003eReplacing \u003ccode\u003e.\u003c/code\u003e and \u003ccode\u003e-\u003c/code\u003e with \u003ccode\u003e_\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003ePrepending \u003ccode\u003eLEAKWATCH_\u003c/code\u003e.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eFor example, the config key \u003ccode\u003escan.concurrency\u003c/code\u003e becomes \u003ccode\u003eLEAKWATCH_SCAN_CONCURRENCY\u003c/code\u003e.\u003c/p\u003e\n\u003ch2 id=\"variable-reference\"\u003eVariable reference\u003c/h2\u003e\n\u003ch3 id=\"leakwatch-specific-variables\"\u003eLeakwatch-specific variables\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eVariable\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_SLACK_TOKEN\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSlack bot token for \u003ccode\u003escan slack\u003c/code\u003e. Equivalent to \u003ccode\u003e--token\u003c/code\u003e. Set this instead of passing the token as a flag to avoid it appearing in shell history or CI logs.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_SCAN_CONCURRENCY\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNumber of concurrent scan workers. Equivalent to \u003ccode\u003e--concurrency\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_VERIFICATION_ENABLED\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSet to \u003ccode\u003efalse\u003c/code\u003e to disable live verification globally. Equivalent to \u003ccode\u003e--no-verify\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_VERIFICATION_RATE_LIMIT\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMaximum verification requests per second across all verifiers.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_OUTPUT_FORMAT\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDefault output format (\u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, or \u003ccode\u003egithub\u003c/code\u003e). Equivalent to \u003ccode\u003e--format\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_DETECTION_ENTROPY_THRESHOLD\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMinimum Shannon entropy for a match to be reported. Float value, e.g. \u003ccode\u003e3.5\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"display-variable\"\u003eDisplay variable\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eVariable\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eNO_COLOR\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eWhen set to any non-empty value, disables ANSI color codes in the \u003ccode\u003etable\u003c/code\u003e output formatter. Follows the \u003ca href=\"https://no-color.org\"\u003eno-color.org\u003c/a\u003e convention.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"aws-variables-for-scan-s3-and-aws-secret-verification\"\u003eAWS variables (for \u003ccode\u003escan s3\u003c/code\u003e and AWS secret verification)\u003c/h3\u003e\n\u003cp\u003eThese are standard AWS SDK environment variables. Leakwatch passes them through to the AWS SDK v2 default credential chain.\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eVariable\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eAWS_ACCESS_KEY_ID\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAWS access key ID.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eAWS_SECRET_ACCESS_KEY\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAWS secret access key.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eAWS_SESSION_TOKEN\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAWS session token (for temporary credentials).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eAWS_REGION\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDefault AWS region.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eAWS_PROFILE\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNamed profile from \u003ccode\u003e~/.aws/credentials\u003c/code\u003e to use.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"gcs-variable-for-scan-gcs\"\u003eGCS variable (for \u003ccode\u003escan gcs\u003c/code\u003e)\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eVariable\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eGOOGLE_APPLICATION_CREDENTIALS\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePath to a Google service-account JSON key file. Used by Application Default Credentials when scanning a GCS bucket.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"precedence-example\"\u003ePrecedence example\u003c/h2\u003e\n\u003cp\u003eGiven this setup:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e.leakwatch.yaml\u003c/code\u003e sets \u003ccode\u003eoutput.format: table\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eLEAKWATCH_OUTPUT_FORMAT=json\u003c/code\u003e is set in the environment\u003c/li\u003e\n\u003cli\u003eThe command is run as \u003ccode\u003eleakwatch scan fs .\u003c/code\u003e (no \u003ccode\u003e--format\u003c/code\u003e flag)\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe effective format is \u003ccode\u003ejson\u003c/code\u003e because the environment variable overrides the config file.\u003c/p\u003e\n\u003cp\u003eIf the command is run as \u003ccode\u003eleakwatch scan fs . --format sarif\u003c/code\u003e, the effective format is \u003ccode\u003esarif\u003c/code\u003e because the flag overrides everything.\u003c/p\u003e\n\u003ch2 id=\"credentials-for-verification-vs-credentials-for-scanning\"\u003eCredentials for verification vs. credentials for scanning\u003c/h2\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNote\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eThe AWS and GCP variables above are consumed to \u003cstrong\u003eauthenticate Leakwatch itself\u003c/strong\u003e when it connects to S3 or GCS to retrieve objects for scanning. They are not used to verify found secrets. Verification of a discovered AWS key, for example, uses that discovered key itself to call AWS STS — not the runner's credentials.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"passing-secrets-safely-in-ci\"\u003ePassing secrets safely in CI\u003c/h2\u003e\n\u003cp\u003eIn GitHub Actions, use encrypted secrets:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003eenv:\n LEAKWATCH_SLACK_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eIn GitLab CI, use masked CI/CD variables:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003evariables:\n LEAKWATCH_SLACK_TOKEN: $SLACK_BOT_TOKEN # defined as a masked variable in project settings\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eNever hard-code token values in workflow files or Dockerfiles.\u003c/p\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eConfiguration File\u003c/a\u003e — full \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e key reference.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/cloud-storage\"\u003eCloud Storage Scanning\u003c/a\u003e — \u003ccode\u003escan s3\u003c/code\u003e and \u003ccode\u003escan gcs\u003c/code\u003e credentials.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/slack\"\u003eSlack Scanning\u003c/a\u003e — Slack token scopes and permissions.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Reference\u003c/a\u003e — equivalent command-line flags.\u003c/li\u003e\n\u003c/ul\u003e\n"},"reference/exit-codes":{"title":"Exit Codes","description":"Leakwatch exit code reference and how to use them in scripts and CI pipelines.","html":"\u003ch1 id=\"exit-codes\"\u003eExit Codes\u003c/h1\u003e\n\u003cp\u003eLeakwatch uses a small, well-defined set of exit codes so that CI pipelines and shell scripts can act on scan results without parsing output. Every scan subcommand exits with one of four codes.\u003c/p\u003e\n\u003ch2 id=\"code-reference\"\u003eCode reference\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eCode\u003c/th\u003e\n\u003cth\u003eName\u003c/th\u003e\n\u003cth\u003eMeaning\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eClean\u003c/td\u003e\n\u003ctd\u003eThe scan completed successfully and no findings passed the active filters.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFindings\u003c/td\u003e\n\u003ctd\u003eThe scan completed and one or more secrets were found (and passed the active filters). This takes precedence over every other outcome: if findings were reported, exit code \u003ccode\u003e1\u003c/code\u003e is returned even if the scan was later interrupted or a repository in a multi-repo scan failed.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eError\u003c/td\u003e\n\u003ctd\u003eA hard error occurred — for example, an invalid flag, an unreadable path, or an authentication failure. An \u003ccode\u003eError: ...\u003c/code\u003e message and a usage hint are printed to stderr.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInterrupted\u003c/td\u003e\n\u003ctd\u003eThe scan received \u003ccode\u003eSIGINT\u003c/code\u003e (\u003ccode\u003eCtrl+C\u003c/code\u003e) or \u003ccode\u003eSIGTERM\u003c/code\u003e before it completed, and it had reported \u003cstrong\u003eno\u003c/strong\u003e findings at the point of interruption. Partial results (if any survived filtering) are still written to the configured output before the process exits. A CI pipeline can therefore never observe a clean exit \u003ccode\u003e0\u003c/code\u003e for a scan that did not run to completion.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"how-filters-affect-exit-code-1\"\u003eHow filters affect exit code 1\u003c/h2\u003e\n\u003cp\u003eExit code \u003ccode\u003e1\u003c/code\u003e is only emitted when at least one finding survives all active output filters. The two most relevant filters are:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/strong\u003e — findings below the threshold are suppressed. If all findings are \u003ccode\u003elow\u003c/code\u003e severity and you run with \u003ccode\u003e--min-severity high\u003c/code\u003e, exit code \u003ccode\u003e0\u003c/code\u003e is returned even though secrets exist.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/strong\u003e — only findings confirmed active by live verification are reported. If no active secrets are found, exit code \u003ccode\u003e0\u003c/code\u003e is returned.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThis means exit code \u003ccode\u003e0\u003c/code\u003e means \u0026quot;no findings matched your current filter settings\u0026quot; — not necessarily that the codebase contains no secrets at all.\u003c/p\u003e\n\u003cdiv class=\"callout callout-warn\"\u003e\u003cdiv class=\"callout-label\"\u003eWarning\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eA clean \u003ccode\u003e0\u003c/code\u003e exit under \u003ccode\u003e--only-verified\u003c/code\u003e does not guarantee the codebase is secret-free. Secrets for which verification is unavailable (10 detector types) are always reported as unverified and are suppressed by \u003ccode\u003e--only-verified\u003c/code\u003e. Pair \u003ccode\u003e--only-verified\u003c/code\u003e with a separate unfiltered scan if you need full coverage.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"using-exit-codes-in-shell-scripts\"\u003eUsing exit codes in shell scripts\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e#!/usr/bin/env bash\nset +e\nleakwatch scan fs . --format json --output leakwatch.json --no-verify\nEXIT_CODE=$?\nset -e\n\ncase \u0026quot;$EXIT_CODE\u0026quot; in\n 0)\n echo \u0026quot;No secrets found. Build continues.\u0026quot;\n ;;\n 1)\n echo \u0026quot;Secrets found — review leakwatch.json and remediate before merging.\u0026quot;\n exit 1\n ;;\n 3)\n echo \u0026quot;Scan was interrupted before it completed — treat as untrusted, do not merge.\u0026quot;\n exit 3\n ;;\n *)\n echo \u0026quot;Leakwatch encountered an error (exit $EXIT_CODE).\u0026quot;\n exit \u0026quot;$EXIT_CODE\u0026quot;\n ;;\nesac\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003eset +e\u003c/code\u003e before the scan prevents the shell from exiting on non-zero codes, giving you the chance to capture and handle the code yourself.\u003c/p\u003e\n\u003ch2 id=\"using-exit-codes-in-ci-pipelines\"\u003eUsing exit codes in CI pipelines\u003c/h2\u003e\n\u003cp\u003eMost CI systems treat any non-zero exit code as a step failure. Since Leakwatch exits \u003ccode\u003e1\u003c/code\u003e when secrets are found, the pipeline fails automatically without any extra configuration — simply run the scan command.\u003c/p\u003e\n\u003cp\u003eTo allow the pipeline to continue even when secrets are found (for example, to collect the report without blocking the build), explicitly ignore the exit code:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --format sarif --output results.sarif --no-verify || true\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eOr, in GitLab CI:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003eallow_failure: true\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eOr, in the GitHub Action, set \u003ccode\u003efail-on-findings: \u0026quot;false\u0026quot;\u003c/code\u003e.\u003c/p\u003e\n\u003ch2 id=\"exit-code-2-in-practice\"\u003eExit code 2 in practice\u003c/h2\u003e\n\u003cp\u003eExit code \u003ccode\u003e2\u003c/code\u003e indicates a configuration or runtime error that prevented the scan from running at all. Common causes:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eAn invalid flag value (for example, \u003ccode\u003e--format invalid\u003c/code\u003e).\u003c/li\u003e\n\u003cli\u003eA path that does not exist or is not readable.\u003c/li\u003e\n\u003cli\u003eA missing required argument (for example, \u003ccode\u003escan git\u003c/code\u003e with no URL).\u003c/li\u003e\n\u003cli\u003eAn authentication error when connecting to a cloud source.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe error message is printed to stderr and includes context to help diagnose the problem. For example, an invalid \u003ccode\u003e--format\u003c/code\u003e value produces:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eError: failed to load configuration: unsupported output format: xlsx\n\nRun 'leakwatch scan fs --help' for usage information.\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eValid \u003ccode\u003e--format\u003c/code\u003e values are \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, and \u003ccode\u003egithub\u003c/code\u003e.\u003c/p\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/other-ci\"\u003eOther CI Systems\u003c/a\u003e — how to wire exit codes into GitLab CI, Jenkins, and others.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/github-action\"\u003eGitHub Action\u003c/a\u003e — how the official action maps exit codes to step outcomes.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Reference\u003c/a\u003e — full flag reference.\u003c/li\u003e\n\u003c/ul\u003e\n"},"scanning/cloud-storage":{"title":"Cloud Storage (S3 \u0026 GCS)","description":"Scan AWS S3 and Google Cloud Storage buckets for leaked secrets.","html":"\u003ch1 id=\"cloud-storage-s3--gcs\"\u003eCloud Storage (S3 \u0026amp; GCS)\u003c/h1\u003e\n\u003cp\u003eSecrets regularly end up in cloud storage — exported database dumps, environment files, CI artefacts, and log archives all flow into buckets that may be readable by more people than intended. Leakwatch can scan AWS S3 and Google Cloud Storage buckets object-by-object and flag any secrets it finds before they become an incident.\u003c/p\u003e\n\u003ch2 id=\"aws-s3\"\u003eAWS S3\u003c/h2\u003e\n\u003ch3 id=\"usage\"\u003eUsage\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan s3 \u0026lt;bucket\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThe command takes exactly one argument: the \u003cstrong\u003ebucket name\u003c/strong\u003e (without the \u003ccode\u003es3://\u003c/code\u003e prefix). The scan target is displayed as \u003ccode\u003es3://\u0026lt;bucket\u0026gt;\u003c/code\u003e.\u003c/p\u003e\n\u003ch3 id=\"authentication\"\u003eAuthentication\u003c/h3\u003e\n\u003cp\u003eLeakwatch uses the standard \u003ca href=\"https://docs.aws.amazon.com/sdkref/latest/guide/standardized-credentials.html\"\u003eAWS default credential chain\u003c/a\u003e:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eEnvironment variables (\u003ccode\u003eAWS_ACCESS_KEY_ID\u003c/code\u003e, \u003ccode\u003eAWS_SECRET_ACCESS_KEY\u003c/code\u003e, \u003ccode\u003eAWS_SESSION_TOKEN\u003c/code\u003e).\u003c/li\u003e\n\u003cli\u003eShared credentials file (\u003ccode\u003e~/.aws/credentials\u003c/code\u003e).\u003c/li\u003e\n\u003cli\u003eShared configuration file (\u003ccode\u003e~/.aws/config\u003c/code\u003e).\u003c/li\u003e\n\u003cli\u003eIAM role attached to the instance or task (EC2, ECS, Lambda).\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eNo additional configuration is required if you are already authenticated with the AWS CLI (\u003ccode\u003eaws configure\u003c/code\u003e or an assumed role).\u003c/p\u003e\n\u003ch3 id=\"s3-specific-flags\"\u003eS3-specific flags\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eType\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--prefix\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eScan only objects whose key starts with this prefix.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--region\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003eFrom AWS config\u003c/td\u003e\n\u003ctd\u003eAWS region of the bucket.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"s3-examples\"\u003eS3 examples\u003c/h3\u003e\n\u003cp\u003eScan an entire bucket:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan s3 my-config-bucket\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eScan only objects under a specific key prefix in a given region:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan s3 my-bucket --prefix logs/ --region us-east-1\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eSave results as SARIF:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan s3 my-bucket --format sarif --output s3-results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eTip\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eUse \u003ccode\u003e--prefix\u003c/code\u003e to limit the scan to a relevant sub-path. Scanning a large bucket with millions of objects can be slow and may incur S3 GET request costs. Narrow the prefix to what actually matters — for example \u003ccode\u003econfigs/\u003c/code\u003e or \u003ccode\u003eexports/\u003c/code\u003e.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003chr\u003e\n\u003ch2 id=\"google-cloud-storage\"\u003eGoogle Cloud Storage\u003c/h2\u003e\n\u003ch3 id=\"usage-1\"\u003eUsage\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan gcs \u0026lt;bucket\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThe command takes exactly one argument: the \u003cstrong\u003ebucket name\u003c/strong\u003e (without the \u003ccode\u003egs://\u003c/code\u003e prefix). The scan target is displayed as \u003ccode\u003egs://\u0026lt;bucket\u0026gt;\u003c/code\u003e.\u003c/p\u003e\n\u003ch3 id=\"authentication-1\"\u003eAuthentication\u003c/h3\u003e\n\u003cp\u003eLeakwatch uses \u003ca href=\"https://cloud.google.com/docs/authentication/application-default-credentials\"\u003eApplication Default Credentials (ADC)\u003c/a\u003e. The credential search order is:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003ccode\u003eGOOGLE_APPLICATION_CREDENTIALS\u003c/code\u003e environment variable pointing to a service-account key file.\u003c/li\u003e\n\u003cli\u003eUser credentials set up by \u003ccode\u003egcloud auth application-default login\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003eService account attached to a Google Compute Engine instance, Cloud Run service, or GKE workload.\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"gcs-specific-flags\"\u003eGCS-specific flags\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eType\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--prefix\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eScan only objects whose name starts with this prefix.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--project\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eGCP project ID (required by some ADC configurations).\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"gcs-examples\"\u003eGCS examples\u003c/h3\u003e\n\u003cp\u003eScan an entire bucket with a specific GCP project:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan gcs my-config-bucket --project my-gcp-project\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eScan only objects under a specific prefix:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan gcs my-bucket --project my-gcp-project --prefix exports/\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eOutput as CSV:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan gcs my-bucket --format csv --output gcs-results.csv\n\u003c/code\u003e\u003c/pre\u003e\n\u003chr\u003e\n\u003ch2 id=\"common-scan-flags\"\u003eCommon scan flags\u003c/h2\u003e\n\u003cp\u003eBoth \u003ccode\u003es3\u003c/code\u003e and \u003ccode\u003egcs\u003c/code\u003e support the same common scan flags:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eShort\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-f\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ejson\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOutput format: \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, \u003ccode\u003egithub\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-o\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estdout\u003c/td\u003e\n\u003ctd\u003eWrite results to this file instead of stdout.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-c\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCPU count\u003c/td\u003e\n\u003ctd\u003eNumber of concurrent workers.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--max-file-size\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e10485760\u003c/code\u003e (10 MB)\u003c/td\u003e\n\u003ctd\u003eSkip objects larger than this value (bytes).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--show-raw\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInclude the raw secret value in output.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eObject-key pattern to exclude. Repeatable; combined with \u003ccode\u003efilter.exclude-paths\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-detectors\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eDetector IDs to exclude for this run. Repeatable; combined with \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--no-verify\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDisable secret verification.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eReport only findings confirmed active by verification.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMinimum severity to report: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e, \u003ccode\u003ecritical\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--remediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAttach remediation guidance to each finding.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003ePath-based exclusions (applied to object keys) are configured in \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e under \u003ccode\u003efilter.exclude-paths\u003c/code\u003e, or per-run via \u003ccode\u003e--exclude\u003c/code\u003e. Root-level flags \u003ccode\u003e--config\u003c/code\u003e and \u003ccode\u003e--log-level\u003c/code\u003e (default \u003ccode\u003ewarn\u003c/code\u003e) also apply.\u003c/p\u003e\n\u003ch2 id=\"finding-metadata\"\u003eFinding metadata\u003c/h2\u003e\n\u003cp\u003eAn S3 or GCS finding carries only the fields common to every source, plus \u003ccode\u003efile_path\u003c/code\u003e set to \u003ccode\u003e\u0026lt;bucket\u0026gt;/\u0026lt;object-key\u0026gt;\u003c/code\u003e (there is no separate \u003ccode\u003eline\u003c/code\u003e field for cloud-storage findings).\u003c/p\u003e\n\u003ch2 id=\"exit-codes\"\u003eExit codes\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eCode\u003c/th\u003e\n\u003cth\u003eMeaning\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan completed, no findings.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan completed, findings reported.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan failed (authentication error, bucket not found, etc.).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan was interrupted (\u003ccode\u003eCtrl+C\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e) before completing, and no findings had been reported.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eA scan summary is printed to stderr after every run. Scans cancel gracefully on SIGINT/SIGTERM.\u003c/p\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eQuick Start\u003c/a\u003e — run your first scan in under a minute.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eConfig File\u003c/a\u003e — configure exclusions and other defaults.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/ignoring-findings\"\u003eIgnoring Findings\u003c/a\u003e — suppress known false positives.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eHow Verification Works\u003c/a\u003e — understand verification statuses.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/filesystem\"\u003eFilesystem\u003c/a\u003e — scan a local directory tree.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Reference\u003c/a\u003e — full flag reference for all commands.\u003c/li\u003e\n\u003c/ul\u003e\n"},"scanning/container-images":{"title":"Container Images","description":"Scan OCI and Docker image layers for leaked secrets without a Docker daemon.","html":"\u003ch1 id=\"container-images\"\u003eContainer Images\u003c/h1\u003e\n\u003cp\u003eContainer images are a common hiding place for secrets: API keys baked into environment variables, credentials embedded in build layers, and configuration files copied into image layers and then forgotten. \u003ccode\u003eleakwatch scan image\u003c/code\u003e inspects every layer of an OCI or Docker image and surfaces those secrets before the image is deployed.\u003c/p\u003e\n\u003ch2 id=\"basic-usage\"\u003eBasic usage\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan image \u0026lt;image:tag\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThe command takes exactly one argument: an image reference in standard \u003ccode\u003ename:tag\u003c/code\u003e notation. Leakwatch uses \u003ca href=\"https://github.com/google/go-containerregistry\"\u003ego-containerregistry\u003c/a\u003e to pull and inspect images \u003cstrong\u003edaemonlessly\u003c/strong\u003e — no running Docker daemon is required.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Scan a Docker Hub image\nleakwatch scan image nginx:latest\n\n# Scan a private GitHub Container Registry image\nleakwatch scan image ghcr.io/org/myapp:v1.2.0\n\n# Scan an Amazon ECR image\nleakwatch scan image 123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"supported-registries\"\u003eSupported registries\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eRegistry\u003c/th\u003e\n\u003cth\u003eExample reference\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003eDocker Hub\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003enginx:latest\u003c/code\u003e, \u003ccode\u003emyorg/myapp:1.0.0\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eGitHub Container Registry (GHCR)\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eghcr.io/org/myapp:v1.2.0\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eAmazon ECR\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eGoogle Container Registry (GCR)\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003egcr.io/my-project/myapp:latest\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eAny OCI-compatible registry\u003c/td\u003e\n\u003ctd\u003eStandard \u003ccode\u003eregistry/name:tag\u003c/code\u003e form\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"authentication\"\u003eAuthentication\u003c/h2\u003e\n\u003cp\u003eLeakwatch uses the standard credential keychain used by Docker and other OCI tools. If you are already authenticated via \u003ccode\u003edocker login\u003c/code\u003e (or an equivalent tool such as \u003ccode\u003ecrane\u003c/code\u003e, \u003ccode\u003eskopeo\u003c/code\u003e, or cloud-provider credential helpers), Leakwatch will use those credentials automatically.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Log in to GHCR first\ndocker login ghcr.io\n\n# Then scan — credentials are picked up automatically\nleakwatch scan image ghcr.io/org/private-app:latest\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eFor Amazon ECR, configure the ECR credential helper or set \u003ccode\u003eAWS_ACCESS_KEY_ID\u003c/code\u003e and related environment variables before scanning.\u003c/p\u003e\n\u003ch2 id=\"how-it-scans\"\u003eHow it scans\u003c/h2\u003e\n\u003cp\u003eLeakwatch pulls the image manifest, iterates over each layer in order, and extracts the files within each layer. Each file's content is run through the same detection pipeline as a filesystem scan. Path exclusions from \u003ccode\u003efilter.exclude-paths\u003c/code\u003e in \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e (or \u003ccode\u003e--exclude\u003c/code\u003e) apply here, limiting which file paths inside layers are examined.\u003c/p\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNote\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eAs a defense against decompression-bomb layers, Leakwatch caps the cumulative decompressed bytes it will read: 2 GiB per layer and 10 GiB across the whole image. If a layer or image exceeds its ceiling, the affected layer's scan is truncated (a warning is logged) rather than let decompression run unbounded.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"flags\"\u003eFlags\u003c/h2\u003e\n\u003cp\u003eThere are no image-specific flags. All common scan flags apply:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eShort\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-f\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ejson\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOutput format: \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, \u003ccode\u003egithub\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-o\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estdout\u003c/td\u003e\n\u003ctd\u003eWrite results to this file instead of stdout.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-c\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCPU count\u003c/td\u003e\n\u003ctd\u003eNumber of concurrent workers.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--max-file-size\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e10485760\u003c/code\u003e (10 MB)\u003c/td\u003e\n\u003ctd\u003eSkip files larger than this value (bytes).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--show-raw\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInclude the raw secret value in output.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003ePath pattern to exclude. Repeatable; combined with \u003ccode\u003efilter.exclude-paths\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-detectors\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eDetector IDs to exclude for this run. Repeatable; combined with \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--no-verify\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDisable secret verification.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eReport only findings confirmed active by verification.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMinimum severity to report: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e, \u003ccode\u003ecritical\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--remediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAttach remediation guidance to each finding.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003ePath-based exclusions are configured in \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e under \u003ccode\u003efilter.exclude-paths\u003c/code\u003e, or per-run via \u003ccode\u003e--exclude\u003c/code\u003e. See \u003ca href=\"#/configuration/config-file\"\u003eConfig File\u003c/a\u003e for details.\u003c/p\u003e\n\u003cp\u003eRoot-level flags \u003ccode\u003e--config\u003c/code\u003e and \u003ccode\u003e--log-level\u003c/code\u003e (default \u003ccode\u003ewarn\u003c/code\u003e) also apply.\u003c/p\u003e\n\u003ch2 id=\"examples\"\u003eExamples\u003c/h2\u003e\n\u003cp\u003eScan a Docker Hub image and print results as a table:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan image alpine:3.20 --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eScan a private registry image and save SARIF output:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan image ghcr.io/org/myapp:v1.2.0 --format sarif -o results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eScan and show only verified active secrets:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan image myapp:latest --only-verified --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eInclude remediation guidance in JSON output:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan image myapp:latest --remediation --format json -o image-findings.json\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"finding-metadata\"\u003eFinding metadata\u003c/h2\u003e\n\u003cp\u003eEach finding from an image scan includes layer metadata:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eField\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eimage\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eThe image reference that was scanned.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003elayer\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eThe layer digest where the finding was detected (\u003ccode\u003e\u0026quot;config\u0026quot;\u003c/code\u003e for a finding in the image config, not a layer).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003elayer_idx\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eThe numeric index (0-based) of the layer within the image manifest; \u003ccode\u003e-1\u003c/code\u003e for a finding in the image config.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003efile_path\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eThe path of the file within the layer.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eTip\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eIntegrate container image scanning into your CI/CD pipeline's build stage to catch secrets before the image is pushed to a registry. Use \u003ccode\u003e--format sarif\u003c/code\u003e to upload results directly to GitHub Code Scanning.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"exit-codes\"\u003eExit codes\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eCode\u003c/th\u003e\n\u003cth\u003eMeaning\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan completed, no findings.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan completed, findings reported.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan failed (image not found, authentication error, etc.).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan was interrupted (\u003ccode\u003eCtrl+C\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e) before completing, and no findings had been reported.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eA scan summary is printed to stderr after every run. Scans cancel gracefully on SIGINT/SIGTERM.\u003c/p\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eQuick Start\u003c/a\u003e — run your first scan in under a minute.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/filesystem\"\u003eFilesystem\u003c/a\u003e — scan a local directory tree.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eConfig File\u003c/a\u003e — configure exclusions and other defaults.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/ignoring-findings\"\u003eIgnoring Findings\u003c/a\u003e — suppress known false positives.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eHow Verification Works\u003c/a\u003e — understand verification statuses.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Reference\u003c/a\u003e — full flag reference for all commands.\u003c/li\u003e\n\u003c/ul\u003e\n"},"scanning/filesystem":{"title":"Filesystem","description":"Scan a local directory tree for leaked secrets with leakwatch scan fs.","html":"\u003ch1 id=\"filesystem\"\u003eFilesystem\u003c/h1\u003e\n\u003cp\u003eLocal source code is where secrets most often appear first. The \u003ccode\u003eleakwatch scan fs\u003c/code\u003e command walks every file in a directory tree, runs the full detection pipeline on each one, and reports any findings before they can be committed — or after the fact on an existing codebase.\u003c/p\u003e\n\u003ch2 id=\"basic-usage\"\u003eBasic usage\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs [path...]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003epath\u003c/code\u003e accepts \u003cstrong\u003ezero or more\u003c/strong\u003e arguments; when omitted, Leakwatch scans the current working directory (\u003ccode\u003e.\u003c/code\u003e). Each path may be a directory (walked recursively) or a single file — you can mix files and directories, and pass several of each, in one invocation.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Scan the current directory\nleakwatch scan fs\n\n# Scan a specific project folder\nleakwatch scan fs ./my-project\n\n# Scan a single file\nleakwatch scan fs cmd/main.go\n\n# Scan several files and directories together\nleakwatch scan fs cmd/ main.go internal/config\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"what-the-filesystem-source-skips-automatically\"\u003eWhat the filesystem source skips automatically\u003c/h2\u003e\n\u003cp\u003eTo keep scans fast and noise-free, the filesystem source skips the following without any configuration:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eBinary files\u003c/strong\u003e — detected by the presence of a null byte in the first 8 KB of the file.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eKnown binary extensions\u003c/strong\u003e — common compiled, image, audio, video, and archive formats.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLock files\u003c/strong\u003e — \u003ccode\u003epackage-lock.json\u003c/code\u003e, \u003ccode\u003eyarn.lock\u003c/code\u003e, \u003ccode\u003ePipfile.lock\u003c/code\u003e, and similar.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003e.git\u003c/code\u003e directories\u003c/strong\u003e — the Git object/pack store is never walked or read as plain files, since \u003ccode\u003escan git\u003c/code\u003e is the dedicated way to scan commit history. This applies to any \u003ccode\u003e.git\u003c/code\u003e directory encountered while walking; if you explicitly pass \u003ccode\u003e.git\u003c/code\u003e itself as the scan path, that exemption does not apply and it is scanned normally. Also unaffected: symlinks are always skipped to avoid cycles.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"flags\"\u003eFlags\u003c/h2\u003e\n\u003ch3 id=\"filesystem-specific\"\u003eFilesystem-specific\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eType\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring (repeatable)\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eGlob patterns for paths to exclude. Can be repeated or comma-separated.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"common-scan-flags\"\u003eCommon scan flags\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eShort\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-f\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ejson\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOutput format: \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, \u003ccode\u003egithub\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-o\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estdout\u003c/td\u003e\n\u003ctd\u003eWrite results to this file instead of stdout.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-c\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCPU count\u003c/td\u003e\n\u003ctd\u003eNumber of concurrent workers.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--max-file-size\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e10485760\u003c/code\u003e (10 MB)\u003c/td\u003e\n\u003ctd\u003eSkip files larger than this value (bytes).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--show-raw\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInclude the raw secret value in output.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-detectors\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eDetector IDs to exclude for this run. Repeatable; combined with \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--no-verify\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDisable secret verification.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eReport only findings confirmed active by verification.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMinimum severity to report: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e, \u003ccode\u003ecritical\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--remediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAttach remediation guidance to each finding.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eRoot-level flags \u003ccode\u003e--config\u003c/code\u003e and \u003ccode\u003e--log-level\u003c/code\u003e (default \u003ccode\u003ewarn\u003c/code\u003e) also apply.\u003c/p\u003e\n\u003ch2 id=\"examples\"\u003eExamples\u003c/h2\u003e\n\u003cp\u003eScan the current directory and print a colorized table to the terminal:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eExclude test files and vendor directories, then save SARIF output for GitHub Code Scanning:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . \\\n --exclude \u0026quot;**/*_test.go\u0026quot; \\\n --exclude \u0026quot;vendor/**\u0026quot; \\\n --format sarif \\\n --output results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eLimit file size to 5 MB and increase worker count for a large monorepo:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --max-file-size 5242880 --concurrency 8 --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eShow only high-severity findings and include rotation instructions:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --min-severity high --remediation --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"excluding-paths\"\u003eExcluding paths\u003c/h2\u003e\n\u003cp\u003eThe \u003ccode\u003e--exclude\u003c/code\u003e flag accepts glob patterns and can be specified multiple times or as a comma-separated list:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Two separate flags\nleakwatch scan fs . --exclude \u0026quot;**/*_test.go\u0026quot; --exclude \u0026quot;docs/**\u0026quot;\n\n# Comma-separated\nleakwatch scan fs . --exclude \u0026quot;**/*_test.go,docs/**\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eFor permanent exclusion rules shared across your team, add them to \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e under \u003ccode\u003efilter.exclude-paths\u003c/code\u003e. Those rules apply to every source, not just filesystem scans. You can also create a \u003ccode\u003e.leakwatchignore\u003c/code\u003e file in your project root. See \u003ca href=\"#/configuration/config-file\"\u003eConfig File\u003c/a\u003e and \u003ca href=\"#/configuration/ignoring-findings\"\u003eIgnoring Findings\u003c/a\u003e for details.\u003c/p\u003e\n\u003ch2 id=\"exit-codes\"\u003eExit codes\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eCode\u003c/th\u003e\n\u003cth\u003eMeaning\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan completed, no findings.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan completed, findings reported.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan failed (configuration error, unreadable path, etc.).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan was interrupted (\u003ccode\u003eCtrl+C\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e) before completing, and no findings had been reported.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eA scan summary (source type, target, file count, duration, and finding count) is printed to stderr after every run. Scans cancel gracefully on SIGINT/SIGTERM.\u003c/p\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eTip\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eRun \u003ccode\u003eleakwatch scan fs . --format table\u003c/code\u003e during development to get a quick visual overview. Switch to \u003ccode\u003e--format sarif\u003c/code\u003e in CI pipelines to integrate with GitHub Code Scanning.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eQuick Start\u003c/a\u003e — run your first scan in under a minute.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eConfig File\u003c/a\u003e — configure default format, exclusions, and more.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/ignoring-findings\"\u003eIgnoring Findings\u003c/a\u003e — \u003ccode\u003e.leakwatchignore\u003c/code\u003e and inline suppression.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eHow Verification Works\u003c/a\u003e — understand verification statuses.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/git-history\"\u003eGit History\u003c/a\u003e — scan committed history, not just the working tree.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Reference\u003c/a\u003e — full flag reference for all commands.\u003c/li\u003e\n\u003c/ul\u003e\n"},"scanning/git-history":{"title":"Git History","description":"Scan the full commit history of a local or remote Git repository for leaked secrets.","html":"\u003ch1 id=\"git-history\"\u003eGit History\u003c/h1\u003e\n\u003cp\u003eA secret that was committed and then deleted is still present in every earlier commit, reachable to anyone with repository access. \u003ccode\u003eleakwatch scan git\u003c/code\u003e walks the \u003cem\u003eentire\u003c/em\u003e commit history of a repository — local or remote — and surfaces those secrets before they can be exploited.\u003c/p\u003e\n\u003ch2 id=\"basic-usage\"\u003eBasic usage\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git \u0026lt;url_or_path\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThe command takes exactly one argument: either a \u003cstrong\u003elocal filesystem path\u003c/strong\u003e to a repository (\u003ccode\u003e.\u003c/code\u003e for the current directory) or a \u003cstrong\u003eremote HTTP/HTTPS or SSH URL\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eLeakwatch uses \u003ca href=\"https://github.com/go-git/go-git\"\u003ego-git\u003c/a\u003e for all Git operations — a pure Go implementation with no dependency on a system \u003ccode\u003egit\u003c/code\u003e binary.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Scan the local repository in the current directory\nleakwatch scan git .\n\n# Scan a remote repository over HTTPS\nleakwatch scan git https://github.com/org/repo.git\n\n# Scan over SSH\nleakwatch scan git git@github.com:org/repo.git\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"how-it-scans\"\u003eHow it scans\u003c/h2\u003e\n\u003cp\u003eLeakwatch walks every commit in the history and examines the blobs introduced by each commit. \u003cstrong\u003eBlob-hash deduplication\u003c/strong\u003e ensures that identical file content is scanned only once, no matter how many commits reference it. This keeps scan time proportional to the \u003cem\u003eunique content\u003c/em\u003e in the repository rather than to the raw commit count.\u003c/p\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNote\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eBecause Leakwatch examines commit-by-commit diffs, it finds secrets that were introduced and later deleted — content that is invisible in the current working tree.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"flags\"\u003eFlags\u003c/h2\u003e\n\u003ch3 id=\"git-specific\"\u003eGit-specific\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eType\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--since\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring (YYYY-MM-DD)\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eScan only commits after this date.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--since-commit\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eScan only changes from this commit hash to HEAD (diff-based).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--branch\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eTarget a specific branch instead of the default.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--depth\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eint\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e (full)\u003c/td\u003e\n\u003ctd\u003eClone depth for \u003cstrong\u003eremote repositories only\u003c/strong\u003e. \u003ccode\u003e0\u003c/code\u003e means full history.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"common-scan-flags\"\u003eCommon scan flags\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eShort\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-f\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ejson\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOutput format: \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, \u003ccode\u003egithub\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-o\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estdout\u003c/td\u003e\n\u003ctd\u003eWrite results to this file instead of stdout.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-c\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCPU count\u003c/td\u003e\n\u003ctd\u003eNumber of concurrent workers.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--max-file-size\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e10485760\u003c/code\u003e (10 MB)\u003c/td\u003e\n\u003ctd\u003eSkip blobs larger than this value (bytes).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--show-raw\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInclude the raw secret value in output.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003ePath pattern to exclude. Repeatable; combined with \u003ccode\u003efilter.exclude-paths\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-detectors\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eDetector IDs to exclude for this run. Repeatable; combined with \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--no-verify\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDisable secret verification.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eReport only findings confirmed active by verification.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMinimum severity to report: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e, \u003ccode\u003ecritical\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--remediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAttach remediation guidance to each finding.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eRoot-level flags \u003ccode\u003e--config\u003c/code\u003e and \u003ccode\u003e--log-level\u003c/code\u003e (default \u003ccode\u003ewarn\u003c/code\u003e) also apply.\u003c/p\u003e\n\u003ch2 id=\"examples\"\u003eExamples\u003c/h2\u003e\n\u003cp\u003eScan the full history of the local repository and print a table:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git . --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eScan only commits made after a specific date on the \u003ccode\u003edevelop\u003c/code\u003e branch:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git . --since 2026-02-23 --branch develop\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eScan changes introduced since a specific commit (useful in CI to check only new commits):\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git . --since-commit a1b2c3d\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eDo a shallow clone of a large remote repository to speed up the initial scan:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git https://github.com/org/repo.git --depth 50\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eScan a remote repository and save verified findings only as SARIF:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git https://github.com/org/repo.git \\\n --only-verified \\\n --format sarif \\\n --output git-results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"finding-metadata\"\u003eFinding metadata\u003c/h2\u003e\n\u003cp\u003eEach finding from a Git scan includes commit metadata:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eField\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003erepository\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eURL or path of the scanned repository (credentials stripped).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecommit\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCommit hash where the secret was introduced.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eauthor\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCommit author's name only.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eemail\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCommit author's email address, as a separate field.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edate\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCommit timestamp.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ebranch\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBranch context (when available).\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eTip\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eUse \u003ccode\u003e--since-commit\u003c/code\u003e in pull-request CI jobs to scan only the commits added by the PR. Use \u003ccode\u003e--since \u0026lt;date\u0026gt;\u003c/code\u003e for scheduled nightly scans covering recent activity.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"credential-safety\"\u003eCredential safety\u003c/h2\u003e\n\u003cp\u003eWhen a repository URL contains embedded credentials (for example \u003ccode\u003ehttps://user:TOKEN@host/repo.git\u003c/code\u003e), Leakwatch strips those credentials before writing anything to logs or output, so the token never appears in scan results or CI traces.\u003c/p\u003e\n\u003ch2 id=\"exit-codes\"\u003eExit codes\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eCode\u003c/th\u003e\n\u003cth\u003eMeaning\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan completed, no findings.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan completed, findings reported.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan failed (invalid URL, authentication error, etc.).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan was interrupted (\u003ccode\u003eCtrl+C\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e) before completing, and no findings had been reported.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eA scan summary is printed to stderr after every run. Scans cancel gracefully on SIGINT/SIGTERM.\u003c/p\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eQuick Start\u003c/a\u003e — run your first scan in under a minute.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/multiple-repos\"\u003eMultiple Repositories\u003c/a\u003e — scan several repositories in one command.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/filesystem\"\u003eFilesystem\u003c/a\u003e — scan the working tree instead of history.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eHow Verification Works\u003c/a\u003e — understand verification statuses.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/ignoring-findings\"\u003eIgnoring Findings\u003c/a\u003e — suppress known false positives.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Reference\u003c/a\u003e — full flag reference for all commands.\u003c/li\u003e\n\u003c/ul\u003e\n"},"scanning/multiple-repos":{"title":"Multiple Repositories","description":"Scan several Git repositories concurrently and combine results into a single report.","html":"\u003ch1 id=\"multiple-repositories\"\u003eMultiple Repositories\u003c/h1\u003e\n\u003cp\u003eWhen an organization grows, secrets can land in any of dozens or hundreds of repositories. Checking them one by one is impractical. \u003ccode\u003eleakwatch scan repos\u003c/code\u003e accepts multiple repository URLs and scans them concurrently, merging all findings into a single output — one command, one report.\u003c/p\u003e\n\u003ch2 id=\"basic-usage\"\u003eBasic usage\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan repos \u0026lt;url1\u0026gt; \u0026lt;url2\u0026gt; [url...]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThe command requires \u003cstrong\u003eat least two\u003c/strong\u003e repository URLs. All repositories are cloned, scanned, and cleaned up automatically. The combined finding count and a single scan summary are reported at the end.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan repos \\\n https://github.com/org/api.git \\\n https://github.com/org/web.git\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"how-it-works\"\u003eHow it works\u003c/h2\u003e\n\u003cp\u003eLeakwatch spawns up to \u003ccode\u003e--parallel\u003c/code\u003e repository scans at once. Each repository is:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eCloned from the provided URL (credentials are stripped from logs and output for safety).\u003c/li\u003e\n\u003cli\u003eScanned with the full detection pipeline, using \u003ccode\u003e--concurrency\u003c/code\u003e workers for that repository.\u003c/li\u003e\n\u003cli\u003eCleaned up (the temporary clone is deleted) once the scan completes.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eAll findings from all repositories are collected and written as a single output, as if the scan had been a single-source run. The displayed target is \u003ccode\u003e\u0026lt;N\u0026gt; repositories\u003c/code\u003e.\u003c/p\u003e\n\u003ch2 id=\"flags\"\u003eFlags\u003c/h2\u003e\n\u003ch3 id=\"multi-repo-specific\"\u003eMulti-repo-specific\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eType\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--parallel\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eint\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNumber of repositories to scan in parallel.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"common-scan-flags\"\u003eCommon scan flags\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eShort\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-f\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ejson\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOutput format: \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, \u003ccode\u003egithub\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-o\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estdout\u003c/td\u003e\n\u003ctd\u003eWrite results to this file instead of stdout.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-c\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCPU count\u003c/td\u003e\n\u003ctd\u003eNumber of concurrent workers \u003cstrong\u003eper repository\u003c/strong\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--max-file-size\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e10485760\u003c/code\u003e (10 MB)\u003c/td\u003e\n\u003ctd\u003eSkip blobs larger than this value (bytes).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--show-raw\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInclude the raw secret value in output.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003ePath pattern to exclude from every repository's scan. Repeatable; combined with \u003ccode\u003efilter.exclude-paths\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-detectors\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eDetector IDs to exclude for this run (e.g. \u003ccode\u003eaws-access-key-id\u003c/code\u003e). Repeatable; combined with \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--no-verify\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDisable secret verification.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eReport only findings confirmed active by verification.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMinimum severity to report: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e, \u003ccode\u003ecritical\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--remediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAttach remediation guidance to each finding.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003ePath exclusions from \u003ccode\u003efilter.exclude-paths\u003c/code\u003e in \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e (or \u003ccode\u003e--exclude\u003c/code\u003e) apply to all repositories. Root-level flags \u003ccode\u003e--config\u003c/code\u003e and \u003ccode\u003e--log-level\u003c/code\u003e (default \u003ccode\u003ewarn\u003c/code\u003e) also apply.\u003c/p\u003e\n\u003ch2 id=\"examples\"\u003eExamples\u003c/h2\u003e\n\u003cp\u003eScan two repositories and display results as a table:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan repos \\\n https://github.com/org/api.git \\\n https://github.com/org/web.git \\\n --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eScan five repositories with higher parallelism and save the combined results as SARIF:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan repos \\\n https://github.com/org/api.git \\\n https://github.com/org/web.git \\\n https://github.com/org/infra.git \\\n https://github.com/org/mobile.git \\\n https://github.com/org/docs.git \\\n --parallel 4 \\\n --format sarif \\\n --output all-repos.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eScan with more workers per repository and show only verified findings:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan repos \\\n https://github.com/org/backend.git \\\n https://github.com/org/frontend.git \\\n --concurrency 8 \\\n --only-verified \\\n --format json \\\n --output verified-findings.json\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"tuning-parallelism\"\u003eTuning parallelism\u003c/h2\u003e\n\u003cp\u003eTwo knobs control throughput:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e--parallel\u003c/code\u003e controls how many repository clones and scans run simultaneously. The default of \u003ccode\u003e3\u003c/code\u003e is appropriate for most workloads. Raise it when network bandwidth and CPU headroom allow; lower it on constrained machines.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e--concurrency\u003c/code\u003e (\u003ccode\u003e-c\u003c/code\u003e) controls how many worker goroutines process file blobs \u003cem\u003ewithin\u003c/em\u003e each individual repository. This is the same flag available on all scan commands.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eTotal concurrent operations at peak = \u003ccode\u003e--parallel\u003c/code\u003e × \u003ccode\u003e--concurrency\u003c/code\u003e.\u003c/p\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNote\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eIf one or more repository scans fail (for example, due to a network error or authentication failure), Leakwatch logs the error and continues scanning the remaining repositories. \u003cstrong\u003eFindings take precedence\u003c/strong\u003e: if any finding survives filtering across the whole run, the exit code is \u003ccode\u003e1\u003c/code\u003e even though some repositories failed to scan. Exit code \u003ccode\u003e2\u003c/code\u003e (repository scan failure) is only returned when zero findings were reported overall.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"credential-safety\"\u003eCredential safety\u003c/h2\u003e\n\u003cp\u003eAny embedded credentials in repository URLs (e.g. \u003ccode\u003ehttps://user:TOKEN@host/repo.git\u003c/code\u003e) are stripped before the URL is written to logs, output, or the scan summary.\u003c/p\u003e\n\u003ch2 id=\"exit-codes\"\u003eExit codes\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eCode\u003c/th\u003e\n\u003cth\u003eMeaning\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAll repository scans completed, no findings.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOne or more findings survived filtering — returned even if some repositories failed to scan or the run was interrupted, so a \u0026quot;secrets found\u0026quot; signal is never masked by an unrelated failure.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eZero findings were reported and one or more repository scans failed, or a configuration error occurred.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eThe run was interrupted (\u003ccode\u003eCtrl+C\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e) before completing and zero findings had been reported.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eA scan summary is printed to stderr after every run. Scans cancel gracefully on SIGINT/SIGTERM.\u003c/p\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/git-history\"\u003eGit History\u003c/a\u003e — scan a single repository in depth.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eQuick Start\u003c/a\u003e — run your first scan in under a minute.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eConfig File\u003c/a\u003e — configure shared defaults for all sources.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/ignoring-findings\"\u003eIgnoring Findings\u003c/a\u003e — suppress known false positives.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eHow Verification Works\u003c/a\u003e — understand verification statuses.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Reference\u003c/a\u003e — full flag reference for all commands.\u003c/li\u003e\n\u003c/ul\u003e\n"},"scanning/slack":{"title":"Slack Workspace","description":"Scan Slack channel and DM message text for leaked secrets.","html":"\u003ch1 id=\"slack-workspace\"\u003eSlack Workspace\u003c/h1\u003e\n\u003cp\u003eDevelopers frequently share credentials in chat — a token pasted into a channel for a quick test, a password sent in a DM, or an API key mentioned in an incident thread. \u003ccode\u003eleakwatch scan slack\u003c/code\u003e reads message text across your Slack workspace and flags any secrets it finds.\u003c/p\u003e\n\u003cdiv class=\"callout callout-warn\"\u003e\u003cdiv class=\"callout-label\"\u003eWarning\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eLeakwatch scans \u003cstrong\u003emessage text only\u003c/strong\u003e. Scanning the contents of uploaded files (attachments, snippets) is not implemented. Only the text body of messages is analysed.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"basic-usage\"\u003eBasic usage\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan slack\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThis command takes \u003cstrong\u003eno positional arguments\u003c/strong\u003e. All configuration is provided through flags or environment variables.\u003c/p\u003e\n\u003ch2 id=\"authentication\"\u003eAuthentication\u003c/h2\u003e\n\u003cp\u003eA Slack Bot Token is required. Provide it via the \u003ccode\u003e--token\u003c/code\u003e flag or the \u003ccode\u003eLEAKWATCH_SLACK_TOKEN\u003c/code\u003e environment variable. Using an environment variable is recommended so the token never appears in shell history or process listings.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eexport LEAKWATCH_SLACK_TOKEN=xoxb-...\nleakwatch scan slack\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"required-bot-token-scopes\"\u003eRequired bot token scopes\u003c/h3\u003e\n\u003cp\u003eThe bot token must be associated with a Slack app that has the following OAuth scopes:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eScope\u003c/th\u003e\n\u003cth\u003ePurpose\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003echannels:history\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRead messages in public channels the bot has joined.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egroups:history\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRead messages in private channels the bot has joined.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eim:history\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRead direct messages (required only with \u003ccode\u003e--include-dms\u003c/code\u003e).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003empim:history\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRead group direct messages (required only with \u003ccode\u003e--include-dms\u003c/code\u003e).\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"flags\"\u003eFlags\u003c/h2\u003e\n\u003ch3 id=\"slack-specific\"\u003eSlack-specific\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eType\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eSlack Bot Token. Prefer \u003ccode\u003eLEAKWATCH_SLACK_TOKEN\u003c/code\u003e env var.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--channels\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003eall channels\u003c/td\u003e\n\u003ctd\u003eComma-separated list of channel names to scan.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-channels\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eComma-separated list of channel names to skip.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--since\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring (YYYY-MM-DD)\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eScan messages posted on or after this date.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--include-dms\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ebool\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAlso scan direct messages and group DMs.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--rate-limit\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003efloat\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMaximum Slack API requests per second.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"common-scan-flags\"\u003eCommon scan flags\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eShort\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-f\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ejson\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOutput format: \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, \u003ccode\u003egithub\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-o\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estdout\u003c/td\u003e\n\u003ctd\u003eWrite results to this file instead of stdout.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-c\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCPU count\u003c/td\u003e\n\u003ctd\u003eNumber of concurrent workers.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--max-file-size\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e10485760\u003c/code\u003e (10 MB)\u003c/td\u003e\n\u003ctd\u003eInternal chunk size limit (bytes).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--show-raw\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInclude the raw secret value in output.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-detectors\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eDetector IDs to exclude for this run. Repeatable; combined with \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--no-verify\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDisable secret verification.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eReport only findings confirmed active by verification.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMinimum severity to report: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e, \u003ccode\u003ecritical\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--remediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAttach remediation guidance to each finding.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eRoot-level flags \u003ccode\u003e--config\u003c/code\u003e and \u003ccode\u003e--log-level\u003c/code\u003e (default \u003ccode\u003ewarn\u003c/code\u003e) also apply. Unlike every other scan subcommand, \u003ccode\u003escan slack\u003c/code\u003e has no \u003ccode\u003e--exclude\u003c/code\u003e path-pattern flag — use \u003ccode\u003e--exclude-channels\u003c/code\u003e to skip whole channels instead.\u003c/p\u003e\n\u003ch2 id=\"examples\"\u003eExamples\u003c/h2\u003e\n\u003cp\u003eScan all channels the bot has access to, using an environment variable for the token:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eexport LEAKWATCH_SLACK_TOKEN=xoxb-...\nleakwatch scan slack\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eScan specific channels and limit to messages since the start of the year:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan slack \\\n --channels general,engineering,backend \\\n --since 2026-01-01\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eExclude noisy channels and include direct messages:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan slack \\\n --exclude-channels random,social,giphy \\\n --include-dms\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eReduce the API request rate to avoid Slack rate-limit errors on large workspaces:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan slack --rate-limit 10 --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eSave only verified active findings to a JSON file:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan slack \\\n --only-verified \\\n --format json \\\n --output slack-findings.json\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"finding-metadata\"\u003eFinding metadata\u003c/h2\u003e\n\u003cp\u003eEach finding from a Slack scan includes message and channel metadata:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eField\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003echannel\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eThe Slack channel \u003cstrong\u003eID\u003c/strong\u003e where the finding was detected (e.g. \u003ccode\u003eC0123456\u003c/code\u003e) — not the human-readable name.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003echannel_name\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eThe human-readable channel name (e.g. \u003ccode\u003eengineering\u003c/code\u003e), as a separate field from \u003ccode\u003echannel\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003emessage_user\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSlack user ID of the message author. There is no \u003ccode\u003eauthor\u003c/code\u003e field for Slack findings.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003emessage_ts\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSlack message timestamp (unique message ID).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ethread_ts\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTimestamp of the parent message, present only when the finding is in a threaded reply.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"performance-considerations\"\u003ePerformance considerations\u003c/h2\u003e\n\u003cp\u003eSlack API requests are subject to rate limits enforced by Slack. The \u003ccode\u003e--rate-limit\u003c/code\u003e flag (default \u003ccode\u003e1\u003c/code\u003e request/second) controls how aggressively Leakwatch makes requests. Raise it cautiously for a faster scan on a workspace with generous rate limits, or lower it further if you see \u003ccode\u003e429 Too Many Requests\u003c/code\u003e errors.\u003c/p\u003e\n\u003cp\u003eWhen Slack responds with \u003ccode\u003e429 Too Many Requests\u003c/code\u003e, Leakwatch automatically honors the \u003ccode\u003eRetry-After\u003c/code\u003e header and retries the request rather than failing the scan outright.\u003c/p\u003e\n\u003cp\u003eUse \u003ccode\u003e--channels\u003c/code\u003e to target specific channels rather than scanning the entire workspace on every run. Combine with \u003ccode\u003e--since\u003c/code\u003e to scan only recent messages incrementally.\u003c/p\u003e\n\u003ch2 id=\"exit-codes\"\u003eExit codes\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eCode\u003c/th\u003e\n\u003cth\u003eMeaning\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan completed, no findings.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan completed, findings reported.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan failed (missing token, authentication error, etc.).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan was interrupted (\u003ccode\u003eCtrl+C\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e) before completing, and no findings had been reported.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eA scan summary is printed to stderr after every run. Scans cancel gracefully on SIGINT/SIGTERM.\u003c/p\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eQuick Start\u003c/a\u003e — run your first scan in under a minute.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eConfig File\u003c/a\u003e — configure defaults in \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/ignoring-findings\"\u003eIgnoring Findings\u003c/a\u003e — suppress known false positives.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eHow Verification Works\u003c/a\u003e — understand verification statuses.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/git-history\"\u003eGit History\u003c/a\u003e — scan committed history for secrets.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Reference\u003c/a\u003e — full flag reference for all commands.\u003c/li\u003e\n\u003c/ul\u003e\n"},"verification/how-verification-works":{"title":"How Verification Works","description":"How Leakwatch confirms whether a detected secret is still active, which verification modes it uses, and how to configure or disable verification.","html":"\u003ch1 id=\"how-verification-works\"\u003eHow Verification Works\u003c/h1\u003e\n\u003cp\u003eFinding a secret in a codebase is only half the story. A key that was rotated six months ago is noise; a key that is still live is an active incident. Verification is the step that draws that line — it takes each detected finding and, where possible, confirms whether the secret is currently valid at the provider.\u003c/p\u003e\n\u003ch2 id=\"from-detection-to-verification\"\u003eFrom detection to verification\u003c/h2\u003e\n\u003cp\u003eAfter the scan engine collects findings, the verifier pool picks them up. Each finding carries a \u003ccode\u003edetector_id\u003c/code\u003e; Leakwatch looks up whether a verifier is registered for that ID:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eIf a verifier exists, it runs and returns a status.\u003c/li\u003e\n\u003cli\u003eIf no verifier is registered for that detector type, the finding passes through unchanged with status \u003ccode\u003eunverified\u003c/code\u003e.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"two-verification-modes\"\u003eTwo verification modes\u003c/h2\u003e\n\u003cp\u003eNot all secrets can be verified the same way. Leakwatch uses two distinct approaches depending on what is safe for each credential type.\u003c/p\u003e\n\u003ch3 id=\"live-api-verification\"\u003eLive API verification\u003c/h3\u003e\n\u003cp\u003eFor 48 detector types, Leakwatch makes a \u003cstrong\u003econtrolled, read-only API call\u003c/strong\u003e to the provider — for example, calling \u003ccode\u003ests:GetCallerIdentity\u003c/code\u003e for AWS keys or \u003ccode\u003eGET /user\u003c/code\u003e for GitHub tokens. The call uses only the minimum endpoint required to confirm identity; it never modifies data, creates resources, or triggers billing events.\u003c/p\u003e\n\u003cp\u003eIf the provider returns a success response, the finding is marked \u003ccode\u003everified_active\u003c/code\u003e. If the provider rejects the credential (for example with HTTP 401, or HTTP 403 for a provider whose scoped-key errors are folded into \u0026quot;inactive\u0026quot;), the finding is marked \u003ccode\u003everified_inactive\u003c/code\u003e. A few verifiers (for example SendGrid) distinguish a 403 caused by a narrowly scoped-but-valid key from a genuine rejection and still report \u003ccode\u003everified_active\u003c/code\u003e in that case — see \u003ca href=\"#/verification/verification-coverage\"\u003eVerification Coverage\u003c/a\u003e for provider-specific notes.\u003c/p\u003e\n\u003ch3 id=\"format-validation-only\"\u003eFormat validation only\u003c/h3\u003e\n\u003cp\u003eFor six credential types, no safe live check exists — the provider has no anonymous identity endpoint, a real call would have side effects, or (for \u003ccode\u003ecoinbase-api-key\u003c/code\u003e) the live API requires HMAC request signing with a paired secret that cannot be reliably associated with the key. For these, Leakwatch validates the structure of the credential without making any network request:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eDetector ID\u003c/th\u003e\n\u003cth\u003eWhat is validated\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egcp-service-account\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eJSON structure — \u003ccode\u003etype\u003c/code\u003e, \u003ccode\u003eproject_id\u003c/code\u003e, \u003ccode\u003eprivate_key_id\u003c/code\u003e, \u003ccode\u003eclient_email\u003c/code\u003e fields present\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003erabbitmq-connection-string\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAMQP URL parsed successfully\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esnowflake-credentials\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFormat check only — a valid format proves nothing, result is always \u003ccode\u003eunverified\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eazure-storage-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFormat check\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eazure-entra-secret\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFormat check\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecoinbase-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCharacter-set and length check\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNote\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eEven when the format check passes, the result remains \u003ccode\u003eunverified\u003c/code\u003e. A structurally valid credential may be expired or revoked. These findings always require manual triage.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"verification-statuses\"\u003eVerification statuses\u003c/h2\u003e\n\u003cp\u003eEvery finding in Leakwatch output carries one of four statuses:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eStatus\u003c/th\u003e\n\u003cth\u003eMeaning\u003c/th\u003e\n\u003cth\u003eRecommended action\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003everified_active\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eThe secret was confirmed live by the provider.\u003c/td\u003e\n\u003ctd\u003eTreat as an active incident. Rotate immediately.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003everified_inactive\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eThe provider rejected the credential.\u003c/td\u003e\n\u003ctd\u003eLikely already rotated. Review context and close.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eunverified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNo verifier exists for this type, or format validation returned no result, or verification was disabled.\u003c/td\u003e\n\u003ctd\u003eTriage manually; context determines risk.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003everify_error\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eThe verifier ran but encountered a network error, timeout, or unexpected response.\u003c/td\u003e\n\u003ctd\u003eTreat as potentially active. Retry or triage manually.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"the-verification-engine\"\u003eThe verification engine\u003c/h2\u003e\n\u003cp\u003eVerification runs in a dedicated concurrent worker pool, isolated from the scan worker pool. The defaults are conservative to avoid triggering provider rate limits:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eSetting\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eConfig key\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003eWorker count\u003c/td\u003e\n\u003ctd\u003e4\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003everification.concurrency\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eGlobal rate limit\u003c/td\u003e\n\u003ctd\u003e10 requests/second\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003everification.rate-limit\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003ePer-request timeout\u003c/td\u003e\n\u003ctd\u003e10 s\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003everification.timeout\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eAll three values are tunable under the \u003ccode\u003everification:\u003c/code\u003e block in \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003everification:\n enabled: true\n concurrency: 4\n rate-limit: 10.0 # requests per second (global)\n timeout: 10s\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eTip\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eIf you are scanning a repository that triggers hundreds of findings, consider lowering \u003ccode\u003erate-limit\u003c/code\u003e to 5 or enabling \u003ccode\u003e--only-verified\u003c/code\u003e to keep the verified-active set small and actionable.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"controlling-verification-at-the-command-line\"\u003eControlling verification at the command line\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eDisable verification entirely\u003c/strong\u003e with \u003ccode\u003e--no-verify\u003c/code\u003e (or set \u003ccode\u003everification.enabled: false\u003c/code\u003e in config). Every finding passes through as \u003ccode\u003eunverified\u003c/code\u003e. Use this for offline or air-gapped environments, or when you want the fastest possible scan without touching any provider API.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --no-verify\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003cstrong\u003eShow only confirmed-live secrets\u003c/strong\u003e with \u003ccode\u003e--only-verified\u003c/code\u003e. Everything that is not \u003ccode\u003everified_active\u003c/code\u003e is dropped from the output. This is the fastest way to triage a large result set — you see only the keys you must act on now.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git . --only-verified\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-warn\"\u003e\u003cdiv class=\"callout-label\"\u003eWarning\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003e\u003ccode\u003e--only-verified\u003c/code\u003e silently drops \u003ccode\u003eunverified\u003c/code\u003e and \u003ccode\u003everify_error\u003c/code\u003e findings. Do not use it as your sole filter in a compliance context — some credential types (JWTs, generic API keys, private keys) can never be verified and would always be excluded.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"secret-safety\"\u003eSecret safety\u003c/h2\u003e\n\u003cp\u003eVerification is designed so that the raw secret value never leaves the process boundary in an unsafe way:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eVerifiers pass the secret directly to the provider's HTTP endpoint over TLS — it is never written to disk, emitted to a log, or cached between runs.\u003c/li\u003e\n\u003cli\u003eA verifier that fails to initialise or encounters a panic is caught by the engine, which marks the finding \u003ccode\u003everify_error\u003c/code\u003e and continues rather than crashing the scan.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/verification/verification-coverage\"\u003eVerification Coverage\u003c/a\u003e — which detector types are live-verified, format-validated, or not verifiable at all.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eConfiguration: Config File\u003c/a\u003e — full reference for the \u003ccode\u003everification:\u003c/code\u003e block.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/output/output-formats\"\u003eOutput Formats\u003c/a\u003e — how the verification status appears in JSON, SARIF, CSV, and table output.\u003c/li\u003e\n\u003c/ul\u003e\n"},"verification/verification-coverage":{"title":"Verification Coverage","description":"Which of the 64 built-in detectors are live-verified, format-validated only, or not verifiable — and what that means for triage.","html":"\u003ch1 id=\"verification-coverage\"\u003eVerification Coverage\u003c/h1\u003e\n\u003cp\u003eLeakwatch ships 64 built-in detectors and 54 verifiers, giving a coverage rate of \u003cstrong\u003e84.4%\u003c/strong\u003e (54 of 64 detector types have some form of verification — either live or format-only). This page maps every detector to its verification status so you know what to expect in your output.\u003c/p\u003e\n\u003ch2 id=\"live-verified-48-detector-types\"\u003eLive-verified (48 detector types)\u003c/h2\u003e\n\u003cp\u003eFor these types, Leakwatch makes a controlled, read-only API call to the provider and returns \u003ccode\u003everified_active\u003c/code\u003e or \u003ccode\u003everified_inactive\u003c/code\u003e. No data is created or modified; the call uses the minimum endpoint needed to confirm identity.\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eDetector type\u003c/th\u003e\n\u003cth\u003eProvider\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eaws-access-key-id\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAWS STS (\u003ccode\u003eGetCallerIdentity\u003c/code\u003e)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egithub-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGitHub REST API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egithub-oauth-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGitHub REST API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egitlab-pat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGitLab REST API (targets a self-hosted GitLab host when one is captured alongside the token; falls back to gitlab.com)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eslack-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSlack Web API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eopenai-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOpenAI API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eanthropic-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAnthropic API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edeepseek-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDeepSeek API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ehuggingface-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHugging Face API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esendgrid-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSendGrid Web API (a \u003ccode\u003e403\u003c/code\u003e from a narrowly scoped/restricted key is treated as \u003ccode\u003everified_active\u003c/code\u003e, not inactive, since the key itself is valid — only \u003ccode\u003e401\u003c/code\u003e maps to \u003ccode\u003everified_inactive\u003c/code\u003e)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003emailgun-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMailgun API (auto-detects and calls the correct EU vs. US regional endpoint)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epostmark-server-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePostmark API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003estripe-api-key-live\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eStripe API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003estripe-api-key-test\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eStripe API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edigitalocean-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDigitalOcean API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecloudflare-api-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCloudflare API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eheroku-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHeroku Platform API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003evercel-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eVercel REST API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003enpm-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003enpm Registry API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epypi-api-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePyPI API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003erubygems-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRubyGems API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edockerhub-pat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDocker Hub API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecircleci-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCircleCI API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eterraform-cloud-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTerraform Cloud API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ediscord-bot-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDiscord API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003etelegram-bot-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTelegram Bot API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esentry-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSentry API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epagerduty-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePagerDuty API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003enewrelic-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNew Relic API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egrafana-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGrafana API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edatadog-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDatadog API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esnyk-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSnyk API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003etwilio-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTwilio API (authenticates with the API Key SID paired to its API Key Secret; without the paired secret the result is \u003ccode\u003eunverified\u003c/code\u003e, never a false \u003ccode\u003everified_inactive\u003c/code\u003e)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edoppler-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDoppler API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003elaunchdarkly-sdk-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eLaunchDarkly API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esonarcloud-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSonarCloud API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eshopify-access-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eShopify Admin API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003enotion-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNotion API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003elinear-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eLinear API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003efigma-pat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFigma REST API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eairtable-pat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAirtable API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eokta-api-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOkta API (targets the org domain captured alongside the token)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eauth0-management-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAuth0 Management API (targets the tenant decoded from the token's own JWT \u003ccode\u003eiss\u003c/code\u003e claim)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edatabricks-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDatabricks REST API (calls the workspace host captured alongside the token)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ebitbucket-app-password\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBitbucket REST API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esupabase-service-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSupabase API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003einfura-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInfura API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eteams-webhook\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMicrosoft Teams\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"format-validated-only-6-detector-types\"\u003eFormat-validated only (6 detector types)\u003c/h2\u003e\n\u003cp\u003eThese verifiers run entirely offline. No network request is made. Because a valid format does not prove a credential is active, all six always return \u003ccode\u003eunverified\u003c/code\u003e regardless of whether the format check passes or fails.\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eDetector ID\u003c/th\u003e\n\u003cth\u003eWhat is validated\u003c/th\u003e\n\u003cth\u003eWhy no live check\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egcp-service-account\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eJSON structure (\u003ccode\u003etype\u003c/code\u003e, \u003ccode\u003eproject_id\u003c/code\u003e, \u003ccode\u003eprivate_key_id\u003c/code\u003e, \u003ccode\u003eclient_email\u003c/code\u003e)\u003c/td\u003e\n\u003ctd\u003eLive check requires a GCP OAuth2 token exchange, which has side effects\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003erabbitmq-connection-string\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAMQP URL parsed successfully\u003c/td\u003e\n\u003ctd\u003eNo public unauthenticated health endpoint\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esnowflake-credentials\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePassword length and host substring check\u003c/td\u003e\n\u003ctd\u003eLive check requires a JDBC/ODBC database connection\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eazure-storage-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFormat check\u003c/td\u003e\n\u003ctd\u003eRequires per-account HMAC signing; no generic identity endpoint\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eazure-entra-secret\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFormat check\u003c/td\u003e\n\u003ctd\u003eClient credential flow would create sessions\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecoinbase-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCharacter-set and length check\u003c/td\u003e\n\u003ctd\u003eCoinbase's legacy API authenticates with HMAC-SHA256 request signing that requires the paired secret, which the detector cannot reliably associate with the key; live verification is not attempted so a real key is never misreported as inactive\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"not-verifiable-10-detector-types\"\u003eNot verifiable (10 detector types)\u003c/h2\u003e\n\u003cp\u003eThese detector types have no verifier at all. Findings from them are always \u003ccode\u003eunverified\u003c/code\u003e. This is \u003cstrong\u003enot\u003c/strong\u003e because they are unimportant — they are detected and reported in full — but because no public verification API exists, or because any verification attempt would have side effects.\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eDetector ID\u003c/th\u003e\n\u003cth\u003eReason\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ejwt\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eA JWT can be issued by any party; there is no universal validation endpoint\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eprivate-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNo provider to call; active use cannot be detected remotely\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egeneric-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eUnknown provider by definition\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edatabase-connection-string\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eConnecting would create sessions on the target database\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eredis-connection-string\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eConnecting would open a live connection to the Redis instance\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eftp-credentials\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNo safe read-only FTP probe\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eldap-credentials\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eLDAP bind would create an authenticated session\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eslack-webhook\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eConfirming a webhook is active requires sending a message\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ehashicorp-vault-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eVault token validation requires knowing the Vault endpoint\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ediscord-webhook-url\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eConfirming a webhook is active requires posting a message to it\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNote\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003e\u0026quot;Not verifiable\u0026quot; does not mean \u0026quot;not found\u0026quot;. All 10 of these types are still detected and appear in your output. They require manual triage to determine whether the credential is live and whether it needs rotation.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"coverage-summary\"\u003eCoverage summary\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eCategory\u003c/th\u003e\n\u003cth\u003eCount\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003eLive-verified\u003c/td\u003e\n\u003ctd\u003e48\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eFormat-validated only\u003c/td\u003e\n\u003ctd\u003e6\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eNot verifiable\u003c/td\u003e\n\u003ctd\u003e10\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eTotal detectors\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003e\u003cstrong\u003e64\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eVerifiers (any coverage)\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003e\u003cstrong\u003e54 (84.4%)\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eHow Verification Works\u003c/a\u003e — the two verification modes, statuses, and the verification engine.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/detectors/detector-catalog\"\u003eDetector Catalog\u003c/a\u003e — the full list of built-in detectors with severities.\u003c/li\u003e\n\u003c/ul\u003e\n"}}; +window.LW_MANUAL["en"] = {"ci-cd/docker-usage":{"title":"Docker Usage","description":"Run Leakwatch scans inside a container using the official Docker image.","html":"\u003ch1 id=\"docker-usage\"\u003eDocker Usage\u003c/h1\u003e\n\u003cp\u003eThe official Leakwatch container image lets you run scans without installing anything on the host machine. Because the image is statically compiled with \u003ccode\u003eCGO_ENABLED=0\u003c/code\u003e and runs as a non-root user, it is safe to use in locked-down CI environments and on shared machines where you do not want to modify the host system.\u003c/p\u003e\n\u003ch2 id=\"image-reference\"\u003eImage reference\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eghcr.io/hodetech/leakwatch\n\u003c/code\u003e\u003c/pre\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eTag\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e:latest\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMost recent release\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e:v1.8.0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eExact current-release pin\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e:v1.7\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCurrent minor-version pin (tracks patch releases)\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eThe image is based on Alpine, runs as the non-root user \u003ccode\u003eleakwatch\u003c/code\u003e, uses \u003ccode\u003e/scan\u003c/code\u003e as the working directory, and has \u003ccode\u003eleakwatch\u003c/code\u003e as its entrypoint.\u003c/p\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNote\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eBecause the entrypoint is \u003ccode\u003eleakwatch\u003c/code\u003e, you append the subcommand and flags directly after the image name — for example, \u003ccode\u003eghcr.io/hodetech/leakwatch:latest scan fs /scan\u003c/code\u003e. There is no need to repeat the binary name.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"scanning-a-local-directory\"\u003eScanning a local directory\u003c/h2\u003e\n\u003cp\u003eMount the directory you want to scan to \u003ccode\u003e/scan\u003c/code\u003e inside the container:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003edocker run --rm \\\n -v \u0026quot;$(pwd):/scan\u0026quot; \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan fs /scan\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eTo write results to a file on the host, write the output file into the mounted volume:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003edocker run --rm \\\n -v \u0026quot;$(pwd):/scan\u0026quot; \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan fs /scan --format sarif -o /scan/leakwatch.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThe file \u003ccode\u003eleakwatch.sarif\u003c/code\u003e appears in the current directory on your host after the container exits.\u003c/p\u003e\n\u003ch2 id=\"scanning-a-remote-git-repository\"\u003eScanning a remote Git repository\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003edocker run --rm \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan git https://github.com/org/repo.git --format json\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eNo volume mount is required for remote Git repositories — Leakwatch clones them into a temporary directory inside the container.\u003c/p\u003e\n\u003ch2 id=\"scanning-a-container-image\"\u003eScanning a container image\u003c/h2\u003e\n\u003cp\u003eLeakwatch is daemonless: it pulls image layers directly from the registry without a Docker daemon. This means you can scan a remote image from within the Leakwatch container without mounting the host Docker socket:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003edocker run --rm \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan image registry.example.com/my-app:v2.3.0\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eFor private registries, pass the credentials as environment variables consumed by the registry client (for example, \u003ccode\u003eDOCKER_CONFIG\u003c/code\u003e pointing to a mounted credentials file, or the standard registry environment variables your registry supports).\u003c/p\u003e\n\u003ch2 id=\"passing-a-configuration-file\"\u003ePassing a configuration file\u003c/h2\u003e\n\u003cp\u003eMount \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e into \u003ccode\u003e/scan\u003c/code\u003e so Leakwatch picks it up automatically:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003edocker run --rm \\\n -v \u0026quot;$(pwd):/scan\u0026quot; \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan fs /scan\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eAs long as \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e is in the mounted directory, Leakwatch finds it because \u003ccode\u003e/scan\u003c/code\u003e is both the working directory and the path passed to the scan. If your config file lives elsewhere, mount it explicitly and use \u003ccode\u003e--config\u003c/code\u003e:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003edocker run --rm \\\n -v \u0026quot;$(pwd):/scan\u0026quot; \\\n -v \u0026quot;/path/to/custom-config.yaml:/config/leakwatch.yaml:ro\u0026quot; \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan fs /scan --config /config/leakwatch.yaml\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"passing-environment-variables\"\u003ePassing environment variables\u003c/h2\u003e\n\u003cp\u003eEnvironment variables for cloud scanning and token-based authentication can be injected with \u003ccode\u003e-e\u003c/code\u003e:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# S3 scan with AWS credentials\ndocker run --rm \\\n -e AWS_ACCESS_KEY_ID=AKIA••••••••••••EXAMPLE \\\n -e AWS_SECRET_ACCESS_KEY=••••••••••••••••••••••••••••••••••••••• \\\n -e AWS_REGION=us-east-1 \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan s3 my-bucket\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eFor CI environments, prefer injecting secrets as masked CI variables rather than embedding them in the command line.\u003c/p\u003e\n\u003ch2 id=\"output-file-pattern\"\u003eOutput file pattern\u003c/h2\u003e\n\u003cp\u003eA common Docker pattern in CI is to write results into the mounted volume and then upload or archive the file as a pipeline artifact:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003edocker run --rm \\\n -v \u0026quot;$(pwd):/scan\u0026quot; \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan fs /scan \\\n --format json \\\n --only-verified \\\n -o /scan/leakwatch-results.json\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/installation\"\u003eInstallation\u003c/a\u003e — install the native binary instead of using Docker.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/filesystem\"\u003eFilesystem Scanning\u003c/a\u003e — \u003ccode\u003escan fs\u003c/code\u003e flags and behavior.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/container-images\"\u003eContainer Images\u003c/a\u003e — scanning OCI/Docker image layers for secrets.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/other-ci\"\u003eOther CI Systems\u003c/a\u003e — using the Docker image in GitLab CI and other pipelines.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Reference\u003c/a\u003e — complete flag reference for all subcommands.\u003c/li\u003e\n\u003c/ul\u003e\n"},"ci-cd/github-action":{"title":"GitHub Action","description":"Use the official Leakwatch GitHub Action to scan for secrets in your GitHub workflows.","html":"\u003ch1 id=\"github-action\"\u003eGitHub Action\u003c/h1\u003e\n\u003cp\u003eEvery push to your repository is an opportunity for a secret to slip through. The official \u003cstrong\u003eLeakwatch GitHub Action\u003c/strong\u003e — published on the GitHub Marketplace and used as \u003ccode\u003eHodeTech/Leakwatch@v1\u003c/code\u003e — integrates Leakwatch directly into your GitHub workflow. It downloads the prebuilt Leakwatch binary for the runner (no Go toolchain or compilation step), runs a scan, maps exit codes, writes a job summary, and optionally uploads SARIF results to GitHub Code Scanning — all without any external service dependency.\u003c/p\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNote\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003e\u003cstrong\u003eSupported runners:\u003c/strong\u003e the action runs on Linux (\u003ccode\u003eubuntu-*\u003c/code\u003e) and macOS (\u003ccode\u003emacos-*\u003c/code\u003e) runners. Windows runners are not supported yet; run the scan on a Linux/macOS runner or use the container image \u003ccode\u003eghcr.io/hodetech/leakwatch\u003c/code\u003e.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"quick-start\"\u003eQuick start\u003c/h2\u003e\n\u003cp\u003eThe minimal configuration blocks the workflow when secrets are found:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e# .github/workflows/leakwatch-minimal.yml\nname: Secret scan (minimal)\n\non: [push, pull_request]\n\njobs:\n leakwatch:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: HodeTech/Leakwatch@v1\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eWith only the defaults, the action scans the filesystem (\u003ccode\u003escan-type: fs\u003c/code\u003e), produces SARIF output, skips live verification (\u003ccode\u003eno-verify: true\u003c/code\u003e), and fails the job if any finding is reported.\u003c/p\u003e\n\u003ch2 id=\"full-example-with-sarif-upload\"\u003eFull example with SARIF upload\u003c/h2\u003e\n\u003cp\u003eThe following workflow enables SARIF upload to GitHub Code Scanning, which surfaces findings as security alerts inside the repository:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e# .github/workflows/leakwatch.yml\nname: Secret scan\n\non:\n push:\n branches: [\u0026quot;main\u0026quot;, \u0026quot;develop\u0026quot;]\n pull_request:\n\npermissions:\n contents: read\n security-events: write # required for SARIF upload\n\njobs:\n leakwatch:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n\n - name: Scan for secrets\n uses: HodeTech/Leakwatch@v1\n with:\n scan-type: fs\n path: .\n format: sarif\n no-verify: \u0026quot;true\u0026quot;\n min-severity: low\n sarif-upload: \u0026quot;true\u0026quot;\n fail-on-findings: \u0026quot;true\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNote\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eSARIF upload requires the job to declare \u003ccode\u003epermissions: security-events: write\u003c/code\u003e. Without it, the upload step fails with a 403 error. The \u003ccode\u003econtents: read\u003c/code\u003e permission is also needed for \u003ccode\u003eactions/checkout@v4\u003c/code\u003e.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"inputs\"\u003eInputs\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eInput\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003escan-type\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efs\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan type to run: \u003ccode\u003efs\u003c/code\u003e, \u003ccode\u003egit\u003c/code\u003e, or \u003ccode\u003eimage\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epath\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e.\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePath to scan (for \u003ccode\u003efs\u003c/code\u003e/\u003ccode\u003egit\u003c/code\u003e) or image reference (for \u003ccode\u003eimage\u003c/code\u003e).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eformat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003esarif\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOutput format: \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, or \u003ccode\u003egithub\u003c/code\u003e (inline pull-request annotations).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eoutput\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e``\u003c/td\u003e\n\u003ctd\u003eWrite formatted output to this file (relative to \u003ccode\u003eworking-directory\u003c/code\u003e). Ignored for \u003ccode\u003eformat: github\u003c/code\u003e. When empty and \u003ccode\u003eformat: sarif\u003c/code\u003e, defaults to \u003ccode\u003eresults.sarif\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eonly-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eReport only findings confirmed active by live verification.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eno-verify\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003etrue\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDisable secret verification (no outbound calls to providers).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003emin-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMinimum severity to report: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e, or \u003ccode\u003ecritical\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eremediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInclude remediation guidance in the output.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003econfig\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e``\u003c/td\u003e\n\u003ctd\u003ePath to a \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e configuration file.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003escan-diff\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eauto\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFor \u003ccode\u003egit\u003c/code\u003e scans, scan only commits new to the event. \u003ccode\u003eauto\u003c/code\u003e enables this on \u003ccode\u003epull_request\u003c/code\u003e/\u003ccode\u003epush\u003c/code\u003e, \u003ccode\u003etrue\u003c/code\u003e forces it, \u003ccode\u003efalse\u003c/code\u003e always scans full history. Requires \u003ccode\u003eactions/checkout\u003c/code\u003e with \u003ccode\u003efetch-depth: 0\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eextra-args\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e``\u003c/td\u003e\n\u003ctd\u003eAdditional raw arguments appended to the \u003ccode\u003eleakwatch scan\u003c/code\u003e command (space-separated). Flags the action manages itself (\u003ccode\u003e--format\u003c/code\u003e, \u003ccode\u003e--output\u003c/code\u003e, \u003ccode\u003e--config\u003c/code\u003e, \u003ccode\u003e--show-raw\u003c/code\u003e) are rejected — use the dedicated inputs instead.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eworking-directory\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e.\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDirectory to run the scan from.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esarif-upload\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eUpload SARIF results to GitHub Code Scanning after the scan.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003efail-on-findings\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003etrue\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFail the workflow step when findings are reported (exit code 1). When \u003ccode\u003efalse\u003c/code\u003e, a \u003ccode\u003e::warning::\u003c/code\u003e annotation is emitted instead so the scan does not block the pipeline. Hard errors (exit code ≥ 2) always fail the step regardless of this setting.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eversion\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elatest\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eLeakwatch version to install: \u003ccode\u003elatest\u003c/code\u003e, or a release tag such as \u003ccode\u003ev1.8.0\u003c/code\u003e to pin a specific release.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003erelease-repo\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eHodeTech/Leakwatch\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRepository (\u003ccode\u003eowner/name\u003c/code\u003e) to download the release binary from. Override only for forks or self-hosted mirrors.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"outputs\"\u003eOutputs\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eOutput\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003efindings-count\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e if no findings were reported; \u003ccode\u003e1\u003c/code\u003e if findings were reported. Mirrors the Leakwatch exit code.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esarif-file\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePath to the SARIF output file on the runner (set when \u003ccode\u003eformat: sarif\u003c/code\u003e).\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"verification-in-ci\"\u003eVerification in CI\u003c/h2\u003e\n\u003cp\u003eBy default, \u003ccode\u003eno-verify\u003c/code\u003e is \u003ccode\u003etrue\u003c/code\u003e — live verification is \u003cstrong\u003eoff\u003c/strong\u003e in CI. This keeps the scan fast and avoids making outbound network calls to provider APIs from CI runners, which may be behind a firewall or have rate-limited credentials.\u003c/p\u003e\n\u003cp\u003eTo enable verification in CI, set \u003ccode\u003eno-verify: \u0026quot;false\u0026quot;\u003c/code\u003e:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e- uses: HodeTech/Leakwatch@v1\n with:\n no-verify: \u0026quot;false\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-warn\"\u003e\u003cdiv class=\"callout-label\"\u003eWarning\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eEnabling verification in CI causes Leakwatch to make authenticated API calls to providers (AWS, GitHub, Stripe, etc.) for each candidate finding. Be aware of provider rate limits and ensure the runner has outbound internet access.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"how-sarif-upload-works\"\u003eHow SARIF upload works\u003c/h2\u003e\n\u003cp\u003eWhen \u003ccode\u003esarif-upload: \u0026quot;true\u0026quot;\u003c/code\u003e and \u003ccode\u003eformat: sarif\u003c/code\u003e, the action:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eTells Leakwatch to write output to \u003ccode\u003eresults.sarif\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003eAfter the scan, calls \u003ccode\u003egithub/codeql-action/upload-sarif@v3\u003c/code\u003e with \u003ccode\u003ecategory: leakwatch\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003eGitHub processes the file and surfaces findings as \u003cstrong\u003eCode Scanning alerts\u003c/strong\u003e under the repository's \u003cstrong\u003eSecurity\u003c/strong\u003e tab.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThe upload step runs with \u003ccode\u003eif: always()\u003c/code\u003e, so results are uploaded even when \u003ccode\u003efail-on-findings: \u0026quot;true\u0026quot;\u003c/code\u003e causes the scan step to set a failure.\u003c/p\u003e\n\u003ch2 id=\"using-action-outputs\"\u003eUsing action outputs\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e- name: Scan for secrets\n id: scan\n uses: HodeTech/Leakwatch@v1\n with:\n fail-on-findings: \u0026quot;false\u0026quot; # let the workflow continue\n\n- name: Print result\n run: echo \u0026quot;Findings reported: ${{ steps.scan.outputs.findings-count }}\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"pinning-a-specific-version\"\u003ePinning a specific version\u003c/h2\u003e\n\u003cp\u003eFor reproducible builds, pin \u003ccode\u003eversion\u003c/code\u003e to a specific tag:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e- uses: HodeTech/Leakwatch@v1\n with:\n version: \u0026quot;v1.8.0\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThis downloads the prebuilt \u003ccode\u003ev1.8.0\u003c/code\u003e binary from the \u003ca href=\"https://github.com/HodeTech/Leakwatch/releases\"\u003eLeakwatch releases\u003c/a\u003e and verifies its SHA-256 checksum before running. For maximum supply-chain safety you can also pin the action itself to a commit SHA, e.g. \u003ccode\u003euses: HodeTech/Leakwatch@\u0026lt;sha\u0026gt;\u003c/code\u003e.\u003c/p\u003e\n\u003ch2 id=\"scanning-only-changed-code-pull-request-diff\"\u003eScanning only changed code (pull-request diff)\u003c/h2\u003e\n\u003cp\u003eFor \u003ccode\u003egit\u003c/code\u003e scans the action can limit the scan to the commits a pull request or push actually introduces, instead of the full history. This is faster and surfaces only newly added secrets. It is controlled by \u003ccode\u003escan-diff\u003c/code\u003e (default \u003ccode\u003eauto\u003c/code\u003e) and requires a full checkout so the base commit is available locally:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003ejobs:\n leakwatch:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n with:\n fetch-depth: 0 # required so the PR base commit is present\n - uses: HodeTech/Leakwatch@v1\n with:\n scan-type: git\n path: .\n # scan-diff: auto (default) — on pull_request/push, scans base..HEAD only\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eOn a \u003ccode\u003epull_request\u003c/code\u003e event the action scans from \u003ccode\u003egithub.event.pull_request.base.sha\u003c/code\u003e; on a \u003ccode\u003epush\u003c/code\u003e event from \u003ccode\u003egithub.event.before\u003c/code\u003e. Set \u003ccode\u003escan-diff: \u0026quot;false\u0026quot;\u003c/code\u003e to always scan the full history, or \u003ccode\u003escan-diff: \u0026quot;true\u0026quot;\u003c/code\u003e to force diff mode. \u003ccode\u003escan-diff\u003c/code\u003e has no effect on \u003ccode\u003efs\u003c/code\u003e/\u003ccode\u003eimage\u003c/code\u003e scans.\u003c/p\u003e\n\u003ch2 id=\"inline-pull-request-annotations\"\u003eInline pull-request annotations\u003c/h2\u003e\n\u003cp\u003eSet \u003ccode\u003eformat: github\u003c/code\u003e to emit the findings as GitHub Actions workflow commands, which appear as inline annotations on the pull request's \u003cstrong\u003eFiles changed\u003c/strong\u003e view and in the run log:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e- uses: HodeTech/Leakwatch@v1\n with:\n format: github\n fail-on-findings: \u0026quot;false\u0026quot; # annotate without blocking, if you prefer\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eAnnotations always show the \u003cstrong\u003eredacted\u003c/strong\u003e value only — the raw secret is never written to the (often public) PR UI or logs. Use \u003ccode\u003eformat: github\u003c/code\u003e for fast, visible PR feedback, or \u003ccode\u003eformat: sarif\u003c/code\u003e with \u003ccode\u003esarif-upload: true\u003c/code\u003e to record findings as Code Scanning alerts under the \u003cstrong\u003eSecurity\u003c/strong\u003e tab.\u003c/p\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/output/output-formats\"\u003eOutput Formats\u003c/a\u003e — understanding JSON, SARIF, CSV, and table output.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/exit-codes\"\u003eExit Codes\u003c/a\u003e — how exit codes map to scan outcomes.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eHow Verification Works\u003c/a\u003e — when and how Leakwatch calls provider APIs.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/pre-commit\"\u003ePre-commit Hook\u003c/a\u003e — catch secrets before they are committed.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/other-ci\"\u003eOther CI Systems\u003c/a\u003e — GitLab CI, Jenkins, and generic shell integration.\u003c/li\u003e\n\u003c/ul\u003e\n"},"ci-cd/other-ci":{"title":"Other CI Systems","description":"Integrate Leakwatch into GitLab CI, Jenkins, Bitbucket Pipelines, and any other CI system.","html":"\u003ch1 id=\"other-ci-systems\"\u003eOther CI Systems\u003c/h1\u003e\n\u003cp\u003eBecause Leakwatch is a single static binary with no runtime dependencies, it runs in any CI environment that can execute a shell command — GitLab CI, Jenkins, Bitbucket Pipelines, CircleCI, Azure DevOps, and others. There is no built-in integration for these systems beyond what is described on this page; the pattern is always: install the binary, run the scan, act on the exit code.\u003c/p\u003e\n\u003ch2 id=\"installing-leakwatch-in-ci\"\u003eInstalling Leakwatch in CI\u003c/h2\u003e\n\u003cp\u003eChoose the method that best suits your runner environment:\u003c/p\u003e\n\u003ch3 id=\"via-go-install-requires-go-on-the-runner\"\u003evia \u003ccode\u003ego install\u003c/code\u003e (requires Go on the runner)\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003ego install github.com/HodeTech/leakwatch@latest\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003ePin to a specific version for reproducible builds:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003ego install github.com/HodeTech/leakwatch@v1.8.0\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"via-the-docker-image-no-go-required\"\u003evia the Docker image (no Go required)\u003c/h3\u003e\n\u003cp\u003eUse \u003ccode\u003eghcr.io/hodetech/leakwatch:latest\u003c/code\u003e as a job image or run it with \u003ccode\u003edocker run\u003c/code\u003e. See \u003ca href=\"#/ci-cd/docker-usage\"\u003eDocker Usage\u003c/a\u003e for the full pattern.\u003c/p\u003e\n\u003ch3 id=\"via-a-prebuilt-release-binary\"\u003evia a prebuilt release binary\u003c/h3\u003e\n\u003cp\u003eDownload the appropriate tarball from \u003ca href=\"https://github.com/HodeTech/Leakwatch/releases\"\u003eGitHub Releases\u003c/a\u003e, extract, and place on \u003ccode\u003ePATH\u003c/code\u003e:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003ecurl -LO https://github.com/HodeTech/Leakwatch/releases/latest/download/leakwatch_Linux_amd64.tar.gz\ntar -xzf leakwatch_Linux_amd64.tar.gz\nsudo mv leakwatch /usr/local/bin/leakwatch\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"exit-codes\"\u003eExit codes\u003c/h2\u003e\n\u003cp\u003eLeakwatch exits with one of four codes, which is the primary mechanism for failing a CI build:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eCode\u003c/th\u003e\n\u003cth\u003eMeaning\u003c/th\u003e\n\u003cth\u003eRecommended CI action\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNo findings\u003c/td\u003e\n\u003ctd\u003ePass the pipeline stage\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSecrets found\u003c/td\u003e\n\u003ctd\u003eFail the pipeline stage\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHard error (bad config, unreadable path, etc.)\u003c/td\u003e\n\u003ctd\u003eFail the pipeline stage\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInterrupted (\u003ccode\u003eCtrl+C\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e) before completing, with no findings reported\u003c/td\u003e\n\u003ctd\u003eFail the pipeline stage — the scan did not run to completion, so a clean result cannot be trusted\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eA generic shell snippet that branches on the exit code:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eset +e\nleakwatch scan fs . --format json -o leakwatch.json --no-verify\nEXIT_CODE=$?\nset -e\n\nif [ \u0026quot;$EXIT_CODE\u0026quot; -eq 0 ]; then\n echo \u0026quot;No secrets found.\u0026quot;\nelif [ \u0026quot;$EXIT_CODE\u0026quot; -eq 1 ]; then\n echo \u0026quot;Secrets found — failing build.\u0026quot;\n exit 1\nelse\n echo \u0026quot;Scan error or interruption (exit $EXIT_CODE) — failing build.\u0026quot;\n exit \u0026quot;$EXIT_CODE\u0026quot;\nfi\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"gitlab-ci-example\"\u003eGitLab CI example\u003c/h2\u003e\n\u003cp\u003eThe following \u003ccode\u003e.gitlab-ci.yml\u003c/code\u003e job installs Leakwatch, runs a filesystem scan, and stores the JSON report as a pipeline artifact:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003eleakwatch:\n stage: test\n image: golang:1.25-alpine\n script:\n - go install github.com/HodeTech/leakwatch@v1.8.0\n - leakwatch scan fs . --format json -o leakwatch.json --no-verify\n artifacts:\n when: always\n paths:\n - leakwatch.json\n expire_in: 7 days\n allow_failure: false\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003eallow_failure: false\u003c/code\u003e (the default) means exit code \u003ccode\u003e1\u003c/code\u003e fails the pipeline stage. Set \u003ccode\u003eallow_failure: true\u003c/code\u003e if you want the scan to report without blocking the merge.\u003c/p\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eTip\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eGitLab supports SAST report artifacts. Leakwatch produces SARIF (\u003ccode\u003e--format sarif\u003c/code\u003e), not GitLab's native SAST JSON schema, so use the \u003ccode\u003epaths:\u003c/code\u003e artifact approach rather than the \u003ccode\u003ereports: sast:\u003c/code\u003e key.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"recommendations-for-ci-runners\"\u003eRecommendations for CI runners\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eUse \u003ccode\u003e--no-verify\u003c/code\u003e on runners without outbound internet access.\u003c/strong\u003e Verification makes live API calls to providers (AWS, GitHub, Stripe, etc.). On air-gapped or firewall-restricted runners, these calls time out and slow the scan. Pass \u003ccode\u003e--no-verify\u003c/code\u003e to skip verification entirely:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --no-verify --format sarif -o results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003cstrong\u003eSave output as an artifact.\u003c/strong\u003e Use \u003ccode\u003e--format sarif\u003c/code\u003e or \u003ccode\u003e--format json\u003c/code\u003e with \u003ccode\u003e--output\u003c/code\u003e to write a file that can be stored, uploaded to a vulnerability management platform, or reviewed after the job completes.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSet \u003ccode\u003e--min-severity\u003c/code\u003e\u003c/strong\u003e to focus on the secrets that matter most. In a noisy codebase, start with \u003ccode\u003e--min-severity high\u003c/code\u003e and lower the threshold once you have cleared the backlog.\u003c/p\u003e\n\u003ch2 id=\"azure-devops-example\"\u003eAzure DevOps example\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e- script: |\n go install github.com/HodeTech/leakwatch@v1.8.0\n leakwatch scan fs . --format sarif -o $(Build.ArtifactStagingDirectory)/leakwatch.sarif --no-verify\n displayName: \u0026quot;Leakwatch secret scan\u0026quot;\n\n- task: PublishBuildArtifacts@1\n inputs:\n pathToPublish: \u0026quot;$(Build.ArtifactStagingDirectory)\u0026quot;\n artifactName: \u0026quot;leakwatch-results\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"jenkins-example\"\u003eJenkins example\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-groovy\"\u003estage('Secret scan') {\n steps {\n sh '''\n go install github.com/HodeTech/leakwatch@v1.8.0\n leakwatch scan fs . --format json -o leakwatch.json --no-verify\n '''\n archiveArtifacts artifacts: 'leakwatch.json', allowEmptyArchive: true\n }\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/reference/exit-codes\"\u003eExit Codes\u003c/a\u003e — full reference for all exit code meanings.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/output/output-formats\"\u003eOutput Formats\u003c/a\u003e — JSON, SARIF, CSV, table, and GitHub annotation output.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/docker-usage\"\u003eDocker Usage\u003c/a\u003e — use the container image instead of installing the binary.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/github-action\"\u003eGitHub Action\u003c/a\u003e — the official action for GitHub workflows.\u003c/li\u003e\n\u003c/ul\u003e\n"},"ci-cd/pre-commit":{"title":"Pre-commit Hook","description":"Use the Leakwatch pre-commit hook to scan for secrets before every commit.","html":"\u003ch1 id=\"pre-commit-hook\"\u003ePre-commit Hook\u003c/h1\u003e\n\u003cp\u003eThe cheapest time to catch a secret is before it enters the repository at all. Leakwatch ships a native \u003ca href=\"https://pre-commit.com\"\u003epre-commit\u003c/a\u003e hook that runs \u003ccode\u003eleakwatch scan fs\u003c/code\u003e automatically on every \u003ccode\u003egit commit\u003c/code\u003e, so a leaked API key or password fails the commit rather than appearing in history.\u003c/p\u003e\n\u003ch2 id=\"prerequisites\"\u003ePrerequisites\u003c/h2\u003e\n\u003cp\u003eYou need:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003ePython 3.8+ (pre-commit is a Python tool).\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://pre-commit.com/#install\"\u003epre-commit\u003c/a\u003e installed globally (\u003ccode\u003epip install pre-commit\u003c/code\u003e or \u003ccode\u003ebrew install pre-commit\u003c/code\u003e).\u003c/li\u003e\n\u003cli\u003eGo 1.25+ on \u003ccode\u003ePATH\u003c/code\u003e — the hook language is \u003ccode\u003egolang\u003c/code\u003e, so pre-commit compiles Leakwatch from source on first run.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"configuration\"\u003eConfiguration\u003c/h2\u003e\n\u003cp\u003eAdd a \u003ccode\u003e.pre-commit-config.yaml\u003c/code\u003e file to the root of your repository (or extend an existing one):\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003erepos:\n - repo: https://github.com/HodeTech/Leakwatch\n rev: v1.8.0\n hooks:\n - id: leakwatch\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eInstall the hooks into the local Git repo:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003epre-commit install\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThat is all. From this point on, every \u003ccode\u003egit commit\u003c/code\u003e triggers a filesystem scan. If Leakwatch finds any secrets, the commit is blocked and the findings are printed to the terminal.\u003c/p\u003e\n\u003ch2 id=\"running-manually\"\u003eRunning manually\u003c/h2\u003e\n\u003cp\u003eTo scan the entire repository (not just staged files) at any time:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003epre-commit run --all-files\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eTo run only the Leakwatch hook without triggering others:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003epre-commit run leakwatch --all-files\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"passing-extra-arguments\"\u003ePassing extra arguments\u003c/h2\u003e\n\u003cp\u003eThe hook's default behavior matches \u003ccode\u003eleakwatch scan fs\u003c/code\u003e with no additional flags. You can pass extra arguments via the \u003ccode\u003eargs:\u003c/code\u003e key:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003erepos:\n - repo: https://github.com/HodeTech/Leakwatch\n rev: v1.8.0\n hooks:\n - id: leakwatch\n args:\n - --only-verified\n - --min-severity\n - high\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThis example reports only high-severity secrets that Leakwatch has confirmed are still active — a strict policy suitable for teams that want to avoid false-positive noise without sacrificing coverage.\u003c/p\u003e\n\u003cp\u003eOther useful arguments:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003eargs:\n - --no-verify # skip live verification for faster commits\n - --min-severity\n - medium # suppress low-severity noise\n - --format\n - table # human-readable output in the terminal\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNote\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003e\u003ccode\u003epass_filenames: false\u003c/code\u003e is set in the hook definition, which means the hook always scans the full working tree rather than only the files staged for the current commit. This guarantees that secrets already present in unstaged files are also detected.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"what-the-hook-scans\"\u003eWhat the hook scans\u003c/h2\u003e\n\u003cp\u003eThe hook runs \u003ccode\u003eleakwatch scan fs\u003c/code\u003e against the repository working directory. It uses the same detection pipeline as the CLI: Aho-Corasick pre-filtering, regex validation, entropy calculation, and (unless \u003ccode\u003e--no-verify\u003c/code\u003e is set) live verification.\u003c/p\u003e\n\u003cp\u003eConfiguration in \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e is respected automatically — exclusion patterns, entropy thresholds, and verification settings all apply without any extra hook configuration.\u003c/p\u003e\n\u003ch2 id=\"skipping-the-hook-temporarily\"\u003eSkipping the hook temporarily\u003c/h2\u003e\n\u003cp\u003eTo commit without running the hook (for example, when committing a controlled test fixture that contains a redacted secret):\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eSKIP=leakwatch git commit -m \u0026quot;chore: add test fixture\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-warn\"\u003e\u003cdiv class=\"callout-label\"\u003eWarning\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eUsing \u003ccode\u003eSKIP=leakwatch\u003c/code\u003e bypasses all secret scanning for that commit. Use it only when you have confirmed the content is safe, and prefer \u003ccode\u003e.leakwatchignore\u003c/code\u003e or inline \u003ccode\u003eleakwatch:ignore\u003c/code\u003e comments for permanent suppressions instead.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"keeping-the-hook-version-pinned\"\u003eKeeping the hook version pinned\u003c/h2\u003e\n\u003cp\u003ePin \u003ccode\u003erev:\u003c/code\u003e to a specific tag rather than a branch name. This ensures all developers on the team use the same detector set and the hook does not silently upgrade mid-sprint:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003erev: v1.8.0 # pin; do not use 'main' or 'HEAD'\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eUpdate by running:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003epre-commit autoupdate\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003ewhich bumps \u003ccode\u003erev\u003c/code\u003e to the latest tag and lets you review the change before committing it.\u003c/p\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/filesystem\"\u003eFilesystem Scanning\u003c/a\u003e — the underlying scan command the hook runs.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eConfiguration File\u003c/a\u003e — control exclusions, entropy, and verification in \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/github-action\"\u003eGitHub Action\u003c/a\u003e — scan on every push and pull request in GitHub CI.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/exit-codes\"\u003eExit Codes\u003c/a\u003e — how exit codes map to scan outcomes.\u003c/li\u003e\n\u003c/ul\u003e\n"},"configuration/config-file":{"title":"Configuration File","description":"How to configure Leakwatch with .leakwatch.yaml — full schema, defaults, validation rules, environment overrides, and the leakwatch init command.","html":"\u003ch1 id=\"configuration-file\"\u003eConfiguration File\u003c/h1\u003e\n\u003cp\u003eLeakwatch's behaviour across every scan command is driven by a single YAML file named \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e. Understanding this file lets you tune concurrency, verification, output format, and path filtering once — and have every scan pick it up automatically.\u003c/p\u003e\n\u003ch2 id=\"file-discovery\"\u003eFile discovery\u003c/h2\u003e\n\u003cp\u003eLeakwatch resolves the config file in the following order:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003e--config \u0026lt;path\u0026gt;\u003c/code\u003e flag\u003c/strong\u003e — use an explicit path regardless of the working directory.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCurrent directory\u003c/strong\u003e — \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e in the directory where the command is run.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eHome directory\u003c/strong\u003e — \u003ccode\u003e~/.leakwatch.yaml\u003c/code\u003e as a fallback.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eIf no file is found, built-in defaults are used for every setting. If a file \u003cstrong\u003eis\u003c/strong\u003e found (or \u003ccode\u003e--config\u003c/code\u003e points at one) but fails to parse as valid YAML, that is a fatal error — the scan does not silently fall back to defaults, since doing so could silently widen detection scope beyond what the operator configured.\u003c/p\u003e\n\u003ch2 id=\"generating-a-starter-file\"\u003eGenerating a starter file\u003c/h2\u003e\n\u003cp\u003eThe \u003ccode\u003eleakwatch init\u003c/code\u003e command writes a ready-to-edit file with recommended defaults:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch init\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBy default the file is written to \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e in the current directory. Use \u003ccode\u003e--output\u003c/code\u003e to choose a different path:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch init --output /etc/leakwatch/.leakwatch.yaml\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eIf the target file already exists, \u003ccode\u003eleakwatch init\u003c/code\u003e will refuse to overwrite it and exit with an error. Pass \u003ccode\u003e--force\u003c/code\u003e to overwrite:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch init --force\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"environment-variable-overrides\"\u003eEnvironment variable overrides\u003c/h2\u003e\n\u003cp\u003eEvery config key can be overridden with an environment variable. The naming rule is:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003ePrefix: \u003ccode\u003eLEAKWATCH_\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eReplace \u003ccode\u003e.\u003c/code\u003e and \u003ccode\u003e-\u003c/code\u003e with \u003ccode\u003e_\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eUppercase\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eExamples:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eConfig key\u003c/th\u003e\n\u003cth\u003eEnvironment variable\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003escan.concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_SCAN_CONCURRENCY\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003everification.rate-limit\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_VERIFICATION_RATE_LIMIT\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eoutput.format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_OUTPUT_FORMAT\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edetection.entropy.threshold\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_DETECTION_ENTROPY_THRESHOLD\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"precedence\"\u003ePrecedence\u003c/h2\u003e\n\u003cp\u003eWhen the same setting is specified in multiple places, the highest-priority source wins:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eCommand-line flag (highest)\u003c/li\u003e\n\u003cli\u003eEnvironment variable\u003c/li\u003e\n\u003cli\u003eConfig file value\u003c/li\u003e\n\u003cli\u003eBuilt-in default (lowest)\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"full-schema\"\u003eFull schema\u003c/h2\u003e\n\u003cp\u003eThe annotated schema below shows every supported key, its default value, and valid range.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e# ── Scan engine ──────────────────────────────────────────────────────────────\n\nscan:\n # Number of concurrent file-processing workers.\n # Defaults to the number of logical CPU cores on the host.\n # Must be \u0026gt;= 1.\n concurrency: 8\n\n # Maximum file size to scan, in bytes. Files larger than this limit are\n # skipped entirely. Default is 10 MB (10485760). Must be \u0026gt;= 1.\n max-file-size: 10485760\n\n# ── Detection ─────────────────────────────────────────────────────────────────\n\ndetection:\n entropy:\n # Enable Shannon entropy calculation for each candidate match.\n enabled: true\n\n # Entropy threshold used for display, and to gate the built-in\n # generic-api-key detector plus any custom rule with its own entropy field.\n # Range: 0–8. Default: 4.0.\n # See note below about built-in findings.\n threshold: 4.0\n\n# ── Verification ─────────────────────────────────────────────────────────────\n\nverification:\n # Enable live verification against provider APIs.\n enabled: true\n\n # Per-finding verification-operation timeout, including bounded fallback and\n # request admission. Must be \u0026gt;= 1ms when verification is enabled.\n # Use a duration string (e.g. \u0026quot;10s\u0026quot;, \u0026quot;500ms\u0026quot;) — a bare integer is\n # treated as nanoseconds and will fail validation.\n timeout: 10s\n\n # Number of concurrent verification workers. Must be \u0026gt;= 1.\n concurrency: 4\n\n # Maximum verification requests per second (token-bucket rate limiter).\n # Must be \u0026gt; 0.\n rate-limit: 10.0\n\n# ── Filtering ─────────────────────────────────────────────────────────────────\n\nfilter:\n # Glob patterns for paths to exclude from scanning.\n # Supported glob styles: filepath.Match patterns, ** double-star spanning\n # zero or more path segments, and trailing-slash dir/ patterns that match\n # the named directory at any depth. Each pattern is tested against both the\n # full path and the base filename, so simple patterns like \u0026quot;*.min.js\u0026quot; match\n # nested files without a leading path prefix.\n # Applies to all scan sources. (The --exclude flag, available on every scan\n # subcommand except `scan slack`, adds to this list at run time rather than\n # replacing it.)\n # Default: [] (no exclusions beyond the built-in binary/lock-file skips).\n exclude-paths:\n - \u0026quot;vendor/**\u0026quot;\n - \u0026quot;node_modules/**\u0026quot;\n - \u0026quot;**/*.min.js\u0026quot;\n - \u0026quot;**/*.min.css\u0026quot;\n - \u0026quot;go.sum\u0026quot;\n - \u0026quot;package-lock.json\u0026quot;\n - \u0026quot;yarn.lock\u0026quot;\n\n # Detector IDs to disable entirely. Findings from listed detectors are never\n # produced regardless of other settings. Default: [].\n # The --exclude-detectors flag, available on every scan subcommand, adds to\n # this list at run time rather than replacing it.\n exclude-detectors: []\n\n# ── Output ────────────────────────────────────────────────────────────────────\n\noutput:\n # Output format. One of: json, sarif, csv, table, github. Default: json.\n # The --format / -f flag overrides this at run time.\n format: json\n\n # Write output to this file path instead of stdout. Default: \u0026quot;\u0026quot; (stdout).\n # The --output / -o flag overrides this at run time. A bare path with no\n # extension is auto-suffixed with the format's own extension, and the file\n # is written with 0600 permissions. Ignored when format is \u0026quot;github\u0026quot;, which\n # always writes to stdout.\n file: \u0026quot;\u0026quot;\n\n # Drop findings below this severity level.\n # One of: low, medium, high, critical. Default: \u0026quot;\u0026quot; (show all).\n # The --min-severity flag overrides this at run time.\n severity-threshold: \u0026quot;\u0026quot;\n\n # Include the unredacted secret value in output.\n # Default: false. The --show-raw flag overrides this at run time.\n show-raw: false\n\n# ── Custom rules ──────────────────────────────────────────────────────────────\n\n# Define your own detectors as YAML rules. See the custom rules page for the\n# full rule schema.\n# custom-rules:\n# - id: \u0026quot;my-internal-token\u0026quot;\n# description: \u0026quot;Internal Service Token\u0026quot;\n# regex: \u0026quot;mycompany_[a-zA-Z0-9]{32}\u0026quot;\n# keywords: [\u0026quot;mycompany_\u0026quot;]\n# severity: critical\ncustom-rules: []\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNote\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003e\u003ccode\u003edetection.entropy.enabled\u003c/code\u003e controls whether a computed entropy value is present alongside findings. \u003ccode\u003edetection.entropy.threshold\u003c/code\u003e does not control display; it gates detectors that opt into engine entropy heuristics — currently only the built-in \u003ccode\u003egeneric-api-key\u003c/code\u003e detector. A match whose entropy falls below that threshold is suppressed as a likely placeholder. Custom rules that declare an \u003ccode\u003eentropy\u003c/code\u003e field apply their own independent per-rule threshold. Every structural/format-anchored built-in detector — \u003ccode\u003eaws-access-key-id\u003c/code\u003e, \u003ccode\u003egithub-token\u003c/code\u003e, and the rest — is \u003cstrong\u003enever\u003c/strong\u003e dropped by the engine threshold, regardless of entropy.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"validation\"\u003eValidation\u003c/h2\u003e\n\u003cp\u003eLeakwatch validates the loaded configuration before starting a scan and exits with an error for any of the following:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eCondition\u003c/th\u003e\n\u003cth\u003eError\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003escan.concurrency \u0026lt; 1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInvalid concurrency value\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003escan.max-file-size \u0026lt; 1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInvalid max-file-size value\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eoutput.format\u003c/code\u003e not in \u003ccode\u003ejson|sarif|csv|table|github\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eUnsupported output format\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edetection.entropy.threshold\u003c/code\u003e outside 0–8\u003c/td\u003e\n\u003ctd\u003eInvalid entropy threshold\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eoutput.severity-threshold\u003c/code\u003e not a valid level (when non-empty)\u003c/td\u003e\n\u003ctd\u003eInvalid severity-threshold\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003everification.timeout \u0026lt; 1ms\u003c/code\u003e (when verification enabled)\u003c/td\u003e\n\u003ctd\u003eInvalid verification timeout\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003everification.concurrency \u0026lt; 1\u003c/code\u003e (when verification enabled)\u003c/td\u003e\n\u003ctd\u003eInvalid verification concurrency\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003everification.rate-limit \u0026lt;= 0\u003c/code\u003e (when verification enabled)\u003c/td\u003e\n\u003ctd\u003eInvalid verification rate-limit\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/ignoring-findings\"\u003eIgnoring Findings\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/severity-and-filtering\"\u003eSeverity \u0026amp; Filtering\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/detectors/custom-rules\"\u003eCustom Rules\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/environment-variables\"\u003eEnvironment Variables\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n"},"configuration/ignoring-findings":{"title":"Ignoring Findings","description":"Suppress false positives with .leakwatchignore files, inline ignore markers, and built-in binary and lock-file skips.","html":"\u003ch1 id=\"ignoring-findings\"\u003eIgnoring Findings\u003c/h1\u003e\n\u003cp\u003eNo scanner has zero false positives. Leakwatch gives you three layered mechanisms to suppress the noise: a \u003ccode\u003e.leakwatchignore\u003c/code\u003e file for path-based exclusions, inline markers for line-level suppression, and a set of always-on built-in skips for binary files and common lock files.\u003c/p\u003e\n\u003ch2 id=\"leakwatchignore-file\"\u003e\u003ccode\u003e.leakwatchignore\u003c/code\u003e file\u003c/h2\u003e\n\u003cp\u003eCreate a \u003ccode\u003e.leakwatchignore\u003c/code\u003e file in your repository root (or in the current directory) to exclude paths from the scan results. It uses a gitignore-style syntax:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eLines starting with \u003ccode\u003e#\u003c/code\u003e are comments.\u003c/li\u003e\n\u003cli\u003eBlank lines are skipped.\u003c/li\u003e\n\u003cli\u003eA \u003ccode\u003e!\u003c/code\u003e prefix \u003cstrong\u003enegates\u003c/strong\u003e a pattern, re-including a path that a previous pattern would have excluded.\u003c/li\u003e\n\u003cli\u003eThe \u003cstrong\u003elast matching pattern wins\u003c/strong\u003e — order matters.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"loading-order\"\u003eLoading order\u003c/h3\u003e\n\u003cp\u003eLeakwatch loads the \u003cstrong\u003efirst\u003c/strong\u003e \u003ccode\u003e.leakwatchignore\u003c/code\u003e it finds, checking the scan root first, then the current working directory as a fallback. Only one file is ever used per scan — if a \u003ccode\u003e.leakwatchignore\u003c/code\u003e exists at the scan root, the current-directory file (if any) is never opened, let alone merged. There is no cross-file precedence or merging: place your rules in a single \u003ccode\u003e.leakwatchignore\u003c/code\u003e at the scan root for predictable behavior.\u003c/p\u003e\n\u003ch3 id=\"glob-syntax\"\u003eGlob syntax\u003c/h3\u003e\n\u003cp\u003eThree pattern styles are supported:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eStyle\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003cth\u003eExample\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003eStandard glob\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efilepath.Match\u003c/code\u003e-style, matched against both the full path and the base filename\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e*.pem\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eDouble-star \u003ccode\u003e**\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSpans zero or more path segments\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003etest/fixtures/**\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eTrailing slash \u003ccode\u003edir/\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMatches every file inside the named directory at any depth\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003esnapshots/\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"example-leakwatchignore\"\u003eExample \u003ccode\u003e.leakwatchignore\u003c/code\u003e\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003e# Ignore all test fixture files\ntest/fixtures/**\n\n# Ignore known placeholder keys in documentation\ndocs/examples/\n\n# Ignore files with a specific extension anywhere in the tree\n*.pem.example\n\n# Re-include a specific file excluded by the rule above\n!docs/examples/real-config-sample.yaml\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNote\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003e\u003ccode\u003e.leakwatchignore\u003c/code\u003e filtering is applied \u003cstrong\u003eafter\u003c/strong\u003e the scan completes, based on the file path of each finding. It does not prevent files from being read — it suppresses the findings they produce. To skip files before they are read at all, use \u003ccode\u003efilter.exclude-paths\u003c/code\u003e in the config file or \u003ccode\u003e--exclude\u003c/code\u003e on \u003ccode\u003escan fs\u003c/code\u003e.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"inline-ignore-markers\"\u003eInline ignore markers\u003c/h2\u003e\n\u003cp\u003ePlace a marker directly on any source line to suppress detectors for that specific line. The marker can appear anywhere on the line — typically inside a comment — and is applied by the engine \u003cstrong\u003ebefore\u003c/strong\u003e verification, so an ignored line never triggers a network call.\u003c/p\u003e\n\u003ch3 id=\"suppress-all-detectors-on-a-line\"\u003eSuppress all detectors on a line\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-python\"\u003e# Payment processing configuration\nSTRIPE_KEY = \u0026quot;sk_test_XXXXXXXXXXXXXXXXXXXX\u0026quot; # leakwatch:ignore\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"suppress-a-specific-detector-on-a-line\"\u003eSuppress a specific detector on a line\u003c/h3\u003e\n\u003cp\u003eUse \u003ccode\u003eleakwatch:ignore:\u0026lt;detector-id\u0026gt;\u003c/code\u003e to suppress only one detector while leaving others active:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-go\"\u003e// This token is intentionally a placeholder for documentation\nexampleToken := \u0026quot;ghp_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\u0026quot; // leakwatch:ignore:github-token\n\u003c/code\u003e\u003c/pre\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e# CI environment variable set by the platform — not a real secret\napi_key: \u0026quot;${CI_API_KEY_PLACEHOLDER}\u0026quot; # leakwatch:ignore:generic-api-key\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eTip\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003ePrefer the detector-specific form (\u003ccode\u003eleakwatch:ignore:\u0026lt;detector-id\u0026gt;\u003c/code\u003e) over the generic one whenever possible. It documents which detector you are suppressing and keeps all other detectors active on that line.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"built-in-skips-always-applied\"\u003eBuilt-in skips (always applied)\u003c/h2\u003e\n\u003cp\u003eLeakwatch unconditionally skips the following before running any detector:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eBinary file extensions\u003c/strong\u003e — files with extensions such as \u003ccode\u003e.exe\u003c/code\u003e, \u003ccode\u003e.dll\u003c/code\u003e, \u003ccode\u003e.so\u003c/code\u003e, \u003ccode\u003e.dylib\u003c/code\u003e, \u003ccode\u003e.bin\u003c/code\u003e, \u003ccode\u003e.png\u003c/code\u003e, \u003ccode\u003e.jpg\u003c/code\u003e, \u003ccode\u003e.gif\u003c/code\u003e, \u003ccode\u003e.mp4\u003c/code\u003e, \u003ccode\u003e.zip\u003c/code\u003e, \u003ccode\u003e.tar\u003c/code\u003e, \u003ccode\u003e.gz\u003c/code\u003e, \u003ccode\u003e.pdf\u003c/code\u003e, \u003ccode\u003e.woff\u003c/code\u003e, \u003ccode\u003e.ttf\u003c/code\u003e, and others are never scanned.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eBinary content detection\u003c/strong\u003e — any file whose first 8 KB contains a null byte is treated as binary and skipped, regardless of extension.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eCommon lock files\u003c/strong\u003e — the following filenames are always skipped because they contain hashes and checksums that produce high rates of false positives:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFile\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epackage-lock.json\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eyarn.lock\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epnpm-lock.yaml\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecomposer.lock\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eGemfile.lock\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eCargo.lock\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epoetry.lock\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ego.sum\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ePipfile.lock\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eThese built-in skips cannot be disabled. They are separate from the \u003ccode\u003efilter.exclude-paths\u003c/code\u003e setting and run before any config-based filtering.\u003c/p\u003e\n\u003ch2 id=\"path-based-exclusion-before-scanning\"\u003ePath-based exclusion before scanning\u003c/h2\u003e\n\u003cp\u003eTo exclude paths before they are even read by the scan engine, use \u003ccode\u003efilter.exclude-paths\u003c/code\u003e in your config file:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003efilter:\n exclude-paths:\n - \u0026quot;vendor/**\u0026quot;\n - \u0026quot;node_modules/**\u0026quot;\n - \u0026quot;**/*.min.js\u0026quot;\n - \u0026quot;third-party/\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThis setting applies to \u003cstrong\u003eall scan sources\u003c/strong\u003e (filesystem, Git history, container images, cloud storage, Slack). On every \u003ccode\u003escan\u003c/code\u003e subcommand that scans paths (\u003ccode\u003efs\u003c/code\u003e, \u003ccode\u003egit\u003c/code\u003e, \u003ccode\u003eimage\u003c/code\u003e, \u003ccode\u003es3\u003c/code\u003e, \u003ccode\u003egcs\u003c/code\u003e, \u003ccode\u003erepos\u003c/code\u003e — all except \u003ccode\u003eslack\u003c/code\u003e, which excludes by \u003ccode\u003e--exclude-channels\u003c/code\u003e instead) you can also pass \u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e on the command line, repeatable, which is combined with (not a replacement for) \u003ccode\u003efilter.exclude-paths\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003eTo silence a noisy detector for a single run without editing the config file, pass \u003ccode\u003e--exclude-detectors \u0026lt;id\u0026gt;[,\u0026lt;id\u0026gt;...]\u003c/code\u003e (repeatable) on any scan subcommand — see \u003ca href=\"#/detectors/detector-catalog\"\u003eDetector Catalog\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eSee \u003ca href=\"#/configuration/config-file\"\u003eConfiguration File\u003c/a\u003e for the full config schema and \u003ca href=\"#/configuration/severity-and-filtering\"\u003eSeverity \u0026amp; Filtering\u003c/a\u003e for detector-level and severity-level filtering.\u003c/p\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eConfiguration File\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/severity-and-filtering\"\u003eSeverity \u0026amp; Filtering\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n"},"configuration/severity-and-filtering":{"title":"Severity \u0026 Filtering","description":"Control which findings reach your output using severity thresholds, verified-only mode, detector exclusions, and path exclusions.","html":"\u003ch1 id=\"severity--filtering\"\u003eSeverity \u0026amp; Filtering\u003c/h1\u003e\n\u003cp\u003eA busy codebase can produce many findings. Leakwatch provides several independent filters you can combine to focus on the signals that matter most: severity thresholds drop low-priority noise, verified-only mode surfaces only confirmed live secrets, detector exclusions silence known false-positive sources, and path exclusions remove entire directory trees from scope.\u003c/p\u003e\n\u003ch2 id=\"severity-levels\"\u003eSeverity levels\u003c/h2\u003e\n\u003cp\u003eEvery built-in detector ships with a default severity. The four levels, from lowest to highest priority, are:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eLevel\u003c/th\u003e\n\u003cth\u003eTypical use\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGeneric patterns with a higher false-positive rate\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003emedium\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRecognizable credential formats, unconfirmed\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ehigh\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eWell-structured secrets where exposure is likely significant\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecritical\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eLive secrets confirmed or formats with near-zero false-positive rates\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eThe severity assigned to each detector is listed in the \u003ca href=\"#/detectors/detector-catalog\"\u003eDetector Catalog\u003c/a\u003e.\u003c/p\u003e\n\u003ch2 id=\"--min-severity-drop-findings-below-a-threshold\"\u003e\u003ccode\u003e--min-severity\u003c/code\u003e: drop findings below a threshold\u003c/h2\u003e\n\u003cp\u003ePass \u003ccode\u003e--min-severity \u0026lt;level\u0026gt;\u003c/code\u003e to discard findings whose severity is below the specified level. Only findings at or above the threshold reach the output.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Show only high and critical findings\nleakwatch scan fs . --min-severity high\n\n# Show medium, high, and critical findings\nleakwatch scan fs . --min-severity medium\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eYou can set a persistent default in the config file under \u003ccode\u003eoutput.severity-threshold\u003c/code\u003e. The \u003ccode\u003e--min-severity\u003c/code\u003e flag overrides the config value at run time:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003eoutput:\n severity-threshold: medium\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"--only-verified-confirmed-active-secrets-only\"\u003e\u003ccode\u003e--only-verified\u003c/code\u003e: confirmed active secrets only\u003c/h2\u003e\n\u003cp\u003ePass \u003ccode\u003e--only-verified\u003c/code\u003e to keep only findings whose verification status is \u003ccode\u003everified_active\u003c/code\u003e — secrets that Leakwatch confirmed are still valid by making a controlled read-only call to the provider API. All other findings (\u003ccode\u003eunverified\u003c/code\u003e, \u003ccode\u003everified_inactive\u003c/code\u003e, or \u003ccode\u003everify_error\u003c/code\u003e) are dropped.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --only-verified\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThis flag is most useful in CI pipelines where you want to fail the build \u003cstrong\u003eonly\u003c/strong\u003e on confirmed incidents, not on suspicious patterns that may be placeholders or already-rotated credentials.\u003c/p\u003e\n\u003cp\u003eSee \u003ca href=\"#/verification/how-verification-works\"\u003eHow Verification Works\u003c/a\u003e for which detectors support live verification.\u003c/p\u003e\n\u003ch2 id=\"filterexclude-detectors-disable-specific-detectors\"\u003e\u003ccode\u003efilter.exclude-detectors\u003c/code\u003e: disable specific detectors\u003c/h2\u003e\n\u003cp\u003eTo permanently disable one or more detectors, list their IDs under \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e in the config file. Findings from listed detectors are never produced, regardless of any other setting:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003efilter:\n exclude-detectors:\n - generic-api-key\n - jwt\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eDetector IDs are listed in the \u003ca href=\"#/detectors/detector-catalog\"\u003eDetector Catalog\u003c/a\u003e. Use this setting when a detector consistently produces false positives for your codebase and other suppression mechanisms (inline ignores or \u003ccode\u003e.leakwatchignore\u003c/code\u003e) are not granular enough.\u003c/p\u003e\n\u003cp\u003eTo silence a detector for a single run without editing the config file, pass \u003ccode\u003e--exclude-detectors \u0026lt;id\u0026gt;[,\u0026lt;id\u0026gt;...]\u003c/code\u003e (repeatable) on any scan subcommand. It adds to \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e rather than replacing it.\u003c/p\u003e\n\u003ch2 id=\"filterexclude-paths-skip-paths-before-scanning\"\u003e\u003ccode\u003efilter.exclude-paths\u003c/code\u003e: skip paths before scanning\u003c/h2\u003e\n\u003cp\u003eTo exclude paths before the scan engine reads them, use \u003ccode\u003efilter.exclude-paths\u003c/code\u003e in the config file. The patterns use the same glob syntax as \u003ccode\u003e.leakwatchignore\u003c/code\u003e (standard globs, \u003ccode\u003e**\u003c/code\u003e double-star, and trailing-slash directory patterns), and apply to \u003cstrong\u003eall scan sources\u003c/strong\u003e:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003efilter:\n exclude-paths:\n - \u0026quot;vendor/**\u0026quot;\n - \u0026quot;node_modules/**\u0026quot;\n - \u0026quot;**/*.min.js\u0026quot;\n - \u0026quot;**/*.min.css\u0026quot;\n - \u0026quot;test/fixtures/\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNote\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eThe \u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e flag is the command-line equivalent of \u003ccode\u003efilter.exclude-paths\u003c/code\u003e, and is available on every scan subcommand \u003cstrong\u003eexcept\u003c/strong\u003e \u003ccode\u003escan slack\u003c/code\u003e (which excludes by \u003ccode\u003e--exclude-channels\u003c/code\u003e instead). It is repeatable and adds to \u003ccode\u003efilter.exclude-paths\u003c/code\u003e rather than replacing it.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"combining-filters-in-ci\"\u003eCombining filters in CI\u003c/h2\u003e\n\u003cp\u003eIn a CI pipeline you typically want a low-noise, high-signal run that fails only on real incidents. A recommended combination:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . \\\n --only-verified \\\n --min-severity high \\\n --format sarif \\\n --output results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eWith a config file handling the persistent path exclusions:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003efilter:\n exclude-paths:\n - \u0026quot;vendor/**\u0026quot;\n - \u0026quot;node_modules/**\u0026quot;\n - \u0026quot;test/fixtures/\u0026quot;\n exclude-detectors:\n - generic-api-key\n\noutput:\n severity-threshold: high\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThen override just the format and destination at the command line for CI:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --only-verified --format sarif --output results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eSee \u003ca href=\"#/verification/how-verification-works\"\u003eHow Verification Works\u003c/a\u003e for verification details, \u003ca href=\"#/configuration/ignoring-findings\"\u003eIgnoring Findings\u003c/a\u003e for inline and file-based suppression, and \u003ca href=\"#/configuration/config-file\"\u003eConfiguration File\u003c/a\u003e for the full schema.\u003c/p\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/detectors/detector-catalog\"\u003eDetector Catalog\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eHow Verification Works\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eConfiguration File\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/ignoring-findings\"\u003eIgnoring Findings\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n"},"detectors/custom-rules":{"title":"Custom Rules","description":"How to define your own secret detection patterns in YAML and add them to a Leakwatch scan alongside the 65 built-in detectors.","html":"\u003ch1 id=\"custom-rules\"\u003eCustom Rules\u003c/h1\u003e\n\u003cp\u003eThe 65 built-in detectors cover widely used credential formats, but every organisation has internal tokens, proprietary service keys, or environment-specific patterns that no generic tool can anticipate. Custom rules let you extend Leakwatch with your own patterns — defined in plain YAML, loaded at runtime — without modifying source code or rebuilding the binary.\u003c/p\u003e\n\u003ch2 id=\"where-custom-rules-live\"\u003eWhere custom rules live\u003c/h2\u003e\n\u003cp\u003eCustom rules are defined under a top-level \u003ccode\u003ecustom-rules:\u003c/code\u003e list in your \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e configuration file:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003ecustom-rules:\n - id: acme-internal-token\n description: \u0026quot;ACME Corp internal service token\u0026quot;\n regex: 'acme_[a-z0-9]{32}'\n keywords:\n - acme_\n severity: critical\n entropy: 3.5\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThe rules are registered at runtime when Leakwatch starts. They run alongside the built-in detectors using the same Aho-Corasick pre-filter pipeline.\u003c/p\u003e\n\u003ch2 id=\"rule-fields\"\u003eRule fields\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eField\u003c/th\u003e\n\u003cth\u003eRequired\u003c/th\u003e\n\u003cth\u003eType\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eid\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYes\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003eUnique detector ID. Used in output and in \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e. Must not collide with a built-in detector ID or another custom rule ID.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edescription\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNo\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003eHuman-readable description shown in output.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eregex\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYes\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003eRE2-compatible regular expression. Maximum 4096 characters.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ekeywords\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNo\u003c/td\u003e\n\u003ctd\u003elist of strings\u003c/td\u003e\n\u003ctd\u003eAho-Corasick pre-filter keywords. The regex only runs on chunks that contain at least one of these strings. Omitting this field causes the regex to run on every chunk.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eseverity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNo\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ecritical\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, or \u003ccode\u003elow\u003c/code\u003e. Defaults to \u003ccode\u003emedium\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eentropy\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNo\u003c/td\u003e\n\u003ctd\u003efloat\u003c/td\u003e\n\u003ctd\u003eShannon entropy threshold (0–8). Matches whose entropy is \u003cstrong\u003ebelow\u003c/strong\u003e this value are discarded. Useful for filtering low-randomness false positives.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eTip\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eAlways supply \u003ccode\u003ekeywords\u003c/code\u003e. Even a single short keyword (like a token prefix) dramatically reduces the number of chunks the regex engine processes, keeping scans fast on large repositories. For example, if all your internal tokens begin with \u003ccode\u003eacme_\u003c/code\u003e, set \u003ccode\u003ekeywords: [acme_]\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003eUse \u003ccode\u003eentropy\u003c/code\u003e to suppress matches on placeholder values like \u003ccode\u003eacme_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u003c/code\u003e that satisfy the pattern but are clearly not real secrets. A threshold around 3.0–3.5 is a good starting point.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"collision-handling\"\u003eCollision handling\u003c/h2\u003e\n\u003cp\u003eIf a custom rule's \u003ccode\u003eid\u003c/code\u003e matches an already-registered detector — either a built-in detector or a previously loaded custom rule — the duplicate is \u003cstrong\u003eskipped\u003c/strong\u003e and an error is logged. Leakwatch does not crash; the rest of the rules load normally. Check the log output if a custom rule appears to have no effect.\u003c/p\u003e\n\u003ch2 id=\"verification\"\u003eVerification\u003c/h2\u003e\n\u003cp\u003eCustom rules have no paired verifier. Findings from custom rules are always reported with status \u003ccode\u003eunverified\u003c/code\u003e — they never become \u003ccode\u003everified_active\u003c/code\u003e or \u003ccode\u003everified_inactive\u003c/code\u003e.\u003c/p\u003e\n\u003ch2 id=\"complete-example\"\u003eComplete example\u003c/h2\u003e\n\u003cp\u003eThe following \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e defines two custom rules: one for an internal service token and one for a signing secret used in webhooks.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003ecustom-rules:\n - id: acme-internal-token\n description: \u0026quot;ACME Corp internal service token (format: acme_ + 32 hex chars)\u0026quot;\n regex: 'acme_[a-f0-9]{32}'\n keywords:\n - acme_\n severity: critical\n entropy: 3.2\n\n - id: acme-webhook-signing-secret\n description: \u0026quot;ACME Corp webhook signing secret (format: whsec_ + 40 base64url chars)\u0026quot;\n regex: 'whsec_[A-Za-z0-9_\\-]{40}'\n keywords:\n - whsec_\n severity: high\n entropy: 3.5\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eRun a scan with this config:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --config .leakwatch.yaml\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eSample JSON output for a custom-rule finding (secret value redacted) — a custom-rule finding uses exactly the same \u003ccode\u003eFinding\u003c/code\u003e schema as every built-in detector (see \u003ca href=\"#/output/output-formats\"\u003eOutput Formats\u003c/a\u003e); there is no separate schema for custom rules:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-json\"\u003e{\n \u0026quot;id\u0026quot;: \u0026quot;8a403a46336fc6ffdc847d202b74536c\u0026quot;,\n \u0026quot;detector_id\u0026quot;: \u0026quot;acme-internal-token\u0026quot;,\n \u0026quot;severity\u0026quot;: \u0026quot;critical\u0026quot;,\n \u0026quot;redacted\u0026quot;: \u0026quot;****cdef\u0026quot;,\n \u0026quot;source\u0026quot;: {\n \u0026quot;source_type\u0026quot;: \u0026quot;filesystem\u0026quot;,\n \u0026quot;file_path\u0026quot;: \u0026quot;config/production.env\u0026quot;,\n \u0026quot;line\u0026quot;: 14\n },\n \u0026quot;verification\u0026quot;: {\n \u0026quot;status\u0026quot;: \u0026quot;unverified\u0026quot;\n },\n \u0026quot;detected_at\u0026quot;: \u0026quot;2026-05-23T10:15:30Z\u0026quot;,\n \u0026quot;entropy\u0026quot;: 4.12\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNote\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eThe \u003ccode\u003eredacted\u003c/code\u003e field always masks the actual secret (by default, \u003ccode\u003e****\u003c/code\u003e followed by at most the last 4 characters). The raw value is never written to output unless you explicitly pass \u003ccode\u003e--show-raw\u003c/code\u003e (not recommended outside controlled environments). Custom rules have no verifier, so \u003ccode\u003everification.status\u003c/code\u003e is always \u003ccode\u003eunverified\u003c/code\u003e.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"excluding-a-custom-rule\"\u003eExcluding a custom rule\u003c/h2\u003e\n\u003cp\u003eCustom rules participate in the same filtering as built-in detectors. To disable a custom rule without removing it from config:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003efilter:\n exclude-detectors:\n - acme-internal-token\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eConfiguration: Config File\u003c/a\u003e — full reference for \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e, including where \u003ccode\u003ecustom-rules:\u003c/code\u003e sits in the document structure.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/detectors/detector-catalog\"\u003eDetector Catalog\u003c/a\u003e — the 65 built-in detectors, to check for ID conflicts before naming your custom rule.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/how-it-works\"\u003eHow It Works\u003c/a\u003e — the Aho-Corasick pre-filter pipeline that \u003ccode\u003ekeywords\u003c/code\u003e plugs into.\u003c/li\u003e\n\u003c/ul\u003e\n"},"detectors/detector-catalog":{"title":"Detector Catalog","description":"All 65 built-in detectors grouped by category, with their IDs, what they detect, and their default severity.","html":"\u003ch1 id=\"detector-catalog\"\u003eDetector Catalog\u003c/h1\u003e\n\u003cp\u003eLeakwatch ships \u003cstrong\u003e65 built-in detectors\u003c/strong\u003e that cover a wide range of credential types — from cloud provider access keys and AI API tokens to database connection strings and private cryptographic keys. Each detector has a stable ID and default severity. Verification capability is classified separately as direct-live, context-required, format-only, or unavailable; a registered verifier does not by itself prove live coverage.\u003c/p\u003e\n\u003cp\u003eThis page lists every built-in detector. For verification coverage details see \u003ca href=\"#/verification/verification-coverage\"\u003eVerification Coverage\u003c/a\u003e. To add your own patterns, see \u003ca href=\"#/detectors/custom-rules\"\u003eCustom Rules\u003c/a\u003e.\u003c/p\u003e\n\u003ch2 id=\"how-to-read-this-catalog\"\u003eHow to read this catalog\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eID\u003c/strong\u003e — the stable string identifier used in config and output. Pass it to \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e to skip a detector, or use it with \u003ccode\u003e--min-severity\u003c/code\u003e filtering (\u003ca href=\"#/configuration/severity-and-filtering\"\u003eSeverity and Filtering\u003c/a\u003e).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDetects\u003c/strong\u003e — what the detector is looking for.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSeverity\u003c/strong\u003e — \u003ccode\u003eCritical\u003c/code\u003e, \u003ccode\u003eHigh\u003c/code\u003e, or \u003ccode\u003eMedium\u003c/code\u003e. This is the default; it feeds the \u003ccode\u003e--min-severity\u003c/code\u003e flag and the \u003ccode\u003eoutput.severity-threshold\u003c/code\u003e config key.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"cloud-and-infrastructure\"\u003eCloud and Infrastructure\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eDetects\u003c/th\u003e\n\u003cth\u003eSeverity\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eaws-access-key-id\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAWS Access Key ID\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egcp-service-account\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGCP Service Account Key\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eazure-storage-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAzure Storage Connection String\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eazure-entra-secret\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAzure Entra ID Client Secret\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edigitalocean-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDigitalOcean Personal Access Token\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecloudflare-api-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCloudflare API Token\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eheroku-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHeroku API Key\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003evercel-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eVercel API Token\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eterraform-cloud-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTerraform Cloud/Enterprise API Token\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ehashicorp-vault-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHashiCorp Vault Token\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edoppler-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDoppler Token — service (\u003ccode\u003edp.st.\u003c/code\u003e), personal (\u003ccode\u003edp.pt.\u003c/code\u003e), CLI/config (\u003ccode\u003edp.ct.\u003c/code\u003e), and SCIM (\u003ccode\u003edp.scim.\u003c/code\u003e) tokens\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"ai--ml\"\u003eAI / ML\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eDetects\u003c/th\u003e\n\u003cth\u003eSeverity\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eopenai-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOpenAI API Key — covers project-scoped (\u003ccode\u003esk-proj-\u003c/code\u003e), service-account (\u003ccode\u003esk-svcacct-\u003c/code\u003e), and legacy (\u003ccode\u003esk-\u003c/code\u003e) key formats\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eanthropic-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAnthropic API Key\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edeepseek-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDeepSeek API Key\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ehuggingface-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHugging Face API Token\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"payments-and-commerce\"\u003ePayments and Commerce\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eDetects\u003c/th\u003e\n\u003cth\u003eSeverity\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003estripe-api-key-live\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eStripe Live API Key\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003estripe-api-key-test\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eStripe Test API Key\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecoinbase-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCoinbase API Key\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eshopify-access-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eShopify Access Token\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"dev-tools-ci-and-packages\"\u003eDev Tools, CI, and Packages\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eDetects\u003c/th\u003e\n\u003cth\u003eSeverity\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egithub-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGitHub Personal Access Token — both the classic \u003ccode\u003eghp_\u003c/code\u003e PAT and the fine-grained \u003ccode\u003egithub_pat_\u003c/code\u003e PAT\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egithub-oauth-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGitHub OAuth2 \u0026amp; installation token — \u003ccode\u003egho_\u003c/code\u003e/\u003ccode\u003eghu_\u003c/code\u003e/\u003ccode\u003eghr_\u003c/code\u003e/\u003ccode\u003eghs_\u003c/code\u003e, including new stateless (JWT-format) \u003ccode\u003eghs_\u003c/code\u003e installation tokens\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egitlab-pat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGitLab credential — the classic \u003ccode\u003eglpat-\u003c/code\u003e personal access token plus deploy (\u003ccode\u003egldt-\u003c/code\u003e), runner (\u003ccode\u003eglrt-\u003c/code\u003e), CI/CD build and trigger (\u003ccode\u003eglcbt-\u003c/code\u003e/\u003ccode\u003eglptt-\u003c/code\u003e), OAuth application secret (\u003ccode\u003egloas-\u003c/code\u003e), and feed (\u003ccode\u003eglft-\u003c/code\u003e) prefixes; repository URLs are never trusted for verification routing\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ebitbucket-app-password\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBitbucket App Password\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecircleci-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCircleCI Personal API Token\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003enpm-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNPM Access Token\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epypi-api-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePyPI API Token\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003erubygems-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRubyGems API Key\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edockerhub-pat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDocker Hub Personal Access Token\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esonarcloud-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSonarCloud/SonarQube Token\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esnyk-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSnyk API Key\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edatabricks-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDatabricks Personal Access Token\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003elaunchdarkly-sdk-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eLaunchDarkly SDK Key\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"communication-and-collaboration\"\u003eCommunication and Collaboration\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eDetects\u003c/th\u003e\n\u003cth\u003eSeverity\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eslack-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSlack Bot/User Token\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eslack-webhook\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSlack Webhook URL\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eteams-webhook\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMicrosoft Teams Incoming Webhook URL\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ediscord-bot-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDiscord Bot Token\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ediscord-webhook-url\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDiscord Webhook URL\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003etelegram-bot-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTelegram Bot Token\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003enotion-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNotion Internal Integration Token\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003elinear-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eLinear API Key\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003efigma-pat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFigma Personal Access Token\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eairtable-pat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAirtable Personal Access Token\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"email-and-messaging-delivery\"\u003eEmail and Messaging Delivery\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eDetects\u003c/th\u003e\n\u003cth\u003eSeverity\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esendgrid-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSendGrid API Key\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003emailgun-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMailgun API Key\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epostmark-server-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePostmark Server API Token\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003etwilio-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTwilio API Key Secret explicitly paired with a nearby \u003ccode\u003eSK...\u003c/code\u003e Key SID; a bare SID is not a secret and is not reported\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"monitoring-and-observability\"\u003eMonitoring and Observability\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eDetects\u003c/th\u003e\n\u003cth\u003eSeverity\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edatadog-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDatadog API Key\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003enewrelic-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNew Relic API Key\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egrafana-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGrafana API Key\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esentry-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSentry Auth Token\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epagerduty-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePagerDuty API Key\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"databases-and-connection-strings\"\u003eDatabases and Connection Strings\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eDetects\u003c/th\u003e\n\u003cth\u003eSeverity\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edatabase-connection-string\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDatabase Connection String\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eredis-connection-string\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRedis Connection String\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003erabbitmq-connection-string\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRabbitMQ Connection String\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esnowflake-credentials\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSnowflake Connection Credentials\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esupabase-service-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSupabase Personal Access Token (\u003ccode\u003esbp_\u003c/code\u003e; stable legacy detector ID)\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"identity-and-access\"\u003eIdentity and Access\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eDetects\u003c/th\u003e\n\u003cth\u003eSeverity\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eauth0-management-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAuth0 Management API Token\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eokta-api-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOkta API Token\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eldap-credentials\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eLDAP/LDAPS Bind Credentials\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"web3\"\u003eWeb3\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eDetects\u003c/th\u003e\n\u003cth\u003eSeverity\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003einfura-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInfura API Key\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"generic-and-cryptographic\"\u003eGeneric and Cryptographic\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eDetects\u003c/th\u003e\n\u003cth\u003eSeverity\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egeneric-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGeneric API Key\u003c/td\u003e\n\u003ctd\u003eMedium\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003estructured-config-secret\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eContextual secret value in JSON/JSONC, high-confidence YAML and TOML scalar assignments, XML elements/attributes, or \u003ccode\u003e.env\u003c/code\u003e configuration\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ejwt\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eJSON Web Token\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eprivate-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePrivate Key (RSA, SSH, DSA, EC, PGP), including PKCS8 \u003ccode\u003ePRIVATE KEY\u003c/code\u003e and password-protected \u003ccode\u003eENCRYPTED PRIVATE KEY\u003c/code\u003e armor\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eftp-credentials\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFTP/SFTP Credentials\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003chr\u003e\n\u003cp\u003e\u003cstrong\u003eTotal: 65 built-in detectors.\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"filtering-by-severity\"\u003eFiltering by severity\u003c/h2\u003e\n\u003cp\u003eFindings are filterable by severity using \u003ccode\u003e--min-severity\u003c/code\u003e at the command line or \u003ccode\u003eoutput.severity-threshold\u003c/code\u003e in config. Only findings at or above the specified level are included in the output. See \u003ca href=\"#/configuration/severity-and-filtering\"\u003eSeverity and Filtering\u003c/a\u003e for details.\u003c/p\u003e\n\u003ch2 id=\"excluding-specific-detectors\"\u003eExcluding specific detectors\u003c/h2\u003e\n\u003cp\u003eTo skip one or more detectors entirely, add their IDs to \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e in \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003efilter:\n exclude-detectors:\n - generic-api-key\n - jwt\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eSee \u003ca href=\"#/configuration/severity-and-filtering\"\u003eSeverity and Filtering\u003c/a\u003e for the full filtering reference.\u003c/p\u003e\n\u003ch2 id=\"verification-coverage\"\u003eVerification coverage\u003c/h2\u003e\n\u003cp\u003eSome detectors have a direct-live verifier, some need trusted or companion context, others are format-only, and eleven have no verifier. See \u003ca href=\"#/verification/verification-coverage\"\u003eVerification Coverage\u003c/a\u003e for the complete breakdown.\u003c/p\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/detectors/custom-rules\"\u003eCustom Rules\u003c/a\u003e — define your own detection patterns in YAML.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/verification-coverage\"\u003eVerification Coverage\u003c/a\u003e — which detectors can be live-verified.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/severity-and-filtering\"\u003eSeverity and Filtering\u003c/a\u003e — filtering findings by severity or detector.\u003c/li\u003e\n\u003c/ul\u003e\n"},"getting-started/how-it-works":{"title":"How It Works","description":"Architecture of the Leakwatch scan pipeline: sources, detection, verification, and output.","html":"\u003ch1 id=\"how-it-works\"\u003eHow It Works\u003c/h1\u003e\n\u003cp\u003eUnderstanding the Leakwatch pipeline helps you tune performance, interpret results, and decide which flags to reach for. This page explains what happens from the moment you run a scan command to the moment a finding appears in your output.\u003c/p\u003e\n\u003ch2 id=\"the-pipeline-at-a-glance\"\u003eThe pipeline at a glance\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-mermaid\"\u003eflowchart LR\n A([Source\\nfs / git / image\\ns3 / gcs / slack]) --\u0026gt; B[Worker Pool\\n—concurrency workers]\n B --\u0026gt; C[Aho-Corasick\\nPre-filter]\n C --\u0026gt; D[Regex\\nDetectors]\n D --\u0026gt; E[Inline-ignore\\nCheck]\n E --\u0026gt; F[Verification\\nPool\\n4 workers / 10 rps]\n F --\u0026gt; G[Post-scan\\nFilters]\n G --\u0026gt; H([Output\\njson / sarif\\ncsv / table\\ngithub])\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eEach stage is described in detail below.\u003c/p\u003e\n\u003ch2 id=\"1-source\"\u003e1. Source\u003c/h2\u003e\n\u003cp\u003eEvery scan starts with a \u003cstrong\u003eSource\u003c/strong\u003e — an abstraction that emits chunks of data for the engine to process. Leakwatch ships six sources:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eSource\u003c/th\u003e\n\u003cth\u003eCommand\u003c/th\u003e\n\u003cth\u003eWhat it emits\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003eFilesystem\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003escan fs\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFile contents from a local directory tree\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eGit history\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003escan git\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eEvery blob across the full commit history\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eContainer image\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003escan image\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eLayer contents of an OCI/Docker image, daemonless\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eAWS S3\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003escan s3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eObject contents from an S3 bucket\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eGoogle Cloud Storage\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003escan gcs\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eObject contents from a GCS bucket\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eSlack\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003escan slack\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMessage text from channels/DMs and opt-in text-like attachments\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNote\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eSlack downloads no files by default. \u003ccode\u003e--include-files\u003c/code\u003e enables bounded scanning of text-like attachments and requires Slack's \u003ccode\u003efiles:read\u003c/code\u003e scope.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003cp\u003eChunks flow into a buffered channel consumed by the worker pool.\u003c/p\u003e\n\u003ch2 id=\"2-worker-pool\"\u003e2. Worker pool\u003c/h2\u003e\n\u003cp\u003eThe engine maintains a fixed pool of \u003cstrong\u003egoroutines\u003c/strong\u003e — one per \u003ccode\u003e--concurrency\u003c/code\u003e value (default: number of CPUs). Each worker pulls a chunk from the channel and runs the detection pipeline independently. Because workers share no mutable state, the pool scales linearly with concurrency up to the limits of I/O and memory.\u003c/p\u003e\n\u003cp\u003eScans respond to \u003ccode\u003eSIGINT\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e: when a cancellation signal arrives, the context is cancelled, workers drain their current chunk and stop, and partial results are collected before output is written.\u003c/p\u003e\n\u003ch2 id=\"3-aho-corasick-keyword-pre-filter\"\u003e3. Aho-Corasick keyword pre-filter\u003c/h2\u003e\n\u003cp\u003eRunning 65 detector patterns on every chunk would be slow. Instead, the engine builds a single \u003cstrong\u003eAho-Corasick multi-pattern automaton\u003c/strong\u003e at startup from the keyword lists declared by each detector. For each chunk, this automaton does a single linear pass and returns only the detectors whose keywords appeared in the chunk's bytes.\u003c/p\u003e\n\u003cp\u003eThis means most detectors never run their regex on most chunks. Detectors that declare no keywords always run (they skip the pre-filter and proceed directly to regex).\u003c/p\u003e\n\u003cp\u003eThe Aho-Corasick implementation comes from \u003ca href=\"https://github.com/cloudflare/ahocorasick\"\u003ecloudflare/ahocorasick\u003c/a\u003e.\u003c/p\u003e\n\u003ch2 id=\"4-regex-detectors\"\u003e4. Regex detectors\u003c/h2\u003e\n\u003cp\u003eEach shortlisted detector runs its compiled \u003cstrong\u003eregular expression\u003c/strong\u003e against the chunk bytes. When a pattern matches, the detector returns a \u003ccode\u003eRawFinding\u003c/code\u003e containing:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eThe raw secret bytes (held in memory only for verification; never logged or written to disk).\u003c/li\u003e\n\u003cli\u003eA \u003cstrong\u003eredacted\u003c/strong\u003e representation safe for output.\u003c/li\u003e\n\u003cli\u003eOptional extra metadata (e.g. account ID for an AWS key).\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eLeakwatch ships \u003cstrong\u003e65 built-in detectors\u003c/strong\u003e across 61 packages, covering cloud providers, AI APIs, payment platforms, databases, messaging tools, version control, structured configuration, and more. You can add your own patterns via \u003ca href=\"#/detectors/custom-rules\"\u003ecustom YAML rules\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eAll detectors are registered at compile time using Go's \u003ccode\u003einit()\u003c/code\u003e function and blank imports (ADR-0004). There is no plugin loader or dynamic discovery at runtime.\u003c/p\u003e\n\u003ch2 id=\"5-inline-ignore-check\"\u003e5. Inline-ignore check\u003c/h2\u003e\n\u003cp\u003eBefore a finding is sent to verification, the engine checks whether the source line contains an \u003cstrong\u003einline ignore marker\u003c/strong\u003e:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-go\"\u003e// leakwatch:ignore\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eor a detector-scoped variant:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-go\"\u003e// leakwatch:ignore:aws-access-key-id\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eIf the marker is present, the finding is silently dropped \u003cstrong\u003ebefore any network call is made\u003c/strong\u003e. This is intentional: ignored secrets should never trigger a live API request.\u003c/p\u003e\n\u003ch2 id=\"6-verification\"\u003e6. Verification\u003c/h2\u003e\n\u003cp\u003eAfter detection completes for all chunks, the engine passes findings to a separate \u003cstrong\u003everification worker pool\u003c/strong\u003e (default 4 workers). Verification:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eAdmits each actual API call through both a shared global and a per-detector \u003cstrong\u003erate limiter\u003c/strong\u003e (the configured default is 10 requests per second for each ceiling). Format-only or missing-context paths consume no token; the shared global bucket bounds total traffic but does not guarantee fair provider scheduling.\u003c/li\u003e\n\u003cli\u003eApplies a \u003cstrong\u003eper-finding verification-operation timeout\u003c/strong\u003e (default 10 seconds), including bounded regional fallback and request admission.\u003c/li\u003e\n\u003cli\u003eMakes only \u003cstrong\u003eread-only, non-destructive\u003c/strong\u003e calls to the provider (e.g. \u003ccode\u003ests:GetCallerIdentity\u003c/code\u003e for AWS keys).\u003c/li\u003e\n\u003cli\u003eMarks each finding with one of four statuses: \u003ccode\u003everified_active\u003c/code\u003e, \u003ccode\u003everified_inactive\u003c/code\u003e, \u003ccode\u003eunverified\u003c/code\u003e, or \u003ccode\u003everify_error\u003c/code\u003e.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eLeakwatch registers \u003cstrong\u003e54 verifier implementations\u003c/strong\u003e for 65 detector types (83.1%), but registry presence is not live coverage: 39 are direct-live, 9 require trusted issuer/region/companion context, and 6 are format-only. The remaining 11 types have no verifier. Context-required, format-only, and no-verifier findings remain \u003ccode\u003eunverified\u003c/code\u003e unless their stated live-verification preconditions are met.\u003c/p\u003e\n\u003cp\u003ePass \u003ccode\u003e--no-verify\u003c/code\u003e to skip this stage entirely — useful for fast, offline scans.\u003c/p\u003e\n\u003cp\u003eFor a deep dive into verification behavior and status meanings, see \u003ca href=\"#/verification/how-verification-works\"\u003eHow Verification Works\u003c/a\u003e.\u003c/p\u003e\n\u003ch2 id=\"7-finding-id-and-entropy\"\u003e7. Finding ID and entropy\u003c/h2\u003e\n\u003cp\u003eEach finding receives a \u003cstrong\u003edeterministic ID\u003c/strong\u003e computed as:\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003esha256(detectorID + redacted + filePath + decimal(line) + \u0026quot;:\u0026quot; + decimal(byteOffset))\n → truncated to the first 16 bytes and hex-encoded to a 32-character string\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThe result is a flat 32-character lowercase hex string (e.g. \u003ccode\u003e447b5d2846d08ce25dd3d638cfe911ad\u003c/code\u003e) — \u003cstrong\u003enot\u003c/strong\u003e a dashed UUID. The same secret at the same location always produces the same ID, making it safe to deduplicate findings across runs or track them in issue trackers.\u003c/p\u003e\n\u003cp\u003eWhen \u003ccode\u003edetection.entropy.enabled\u003c/code\u003e is true, \u003cstrong\u003eShannon entropy\u003c/strong\u003e (range 0–8) is computed for each non-empty finding and exposed in output for informational purposes; when disabled, the field is absent rather than a synthetic zero. At the engine level, \u003ccode\u003edetection.entropy.threshold\u003c/code\u003e applies \u003cstrong\u003eonly\u003c/strong\u003e to heuristic detectors that explicitly opt in — currently just \u003ccode\u003egeneric-api-key\u003c/code\u003e — dropping a match whose entropy falls below the threshold to suppress low-randomness placeholders. Every structural (format-anchored) detector, such as \u003ccode\u003eaws-access-key-id\u003c/code\u003e or \u003ccode\u003egithub-token\u003c/code\u003e, is never gated by entropy: a low-entropy match from those detectors still appears in results. Custom rules apply their own independent per-rule \u003ccode\u003eentropy\u003c/code\u003e threshold (see \u003ca href=\"#/detectors/custom-rules\"\u003eCustom Rules\u003c/a\u003e), separate from this engine-level gate.\u003c/p\u003e\n\u003ch2 id=\"8-post-scan-filters\"\u003e8. Post-scan filters\u003c/h2\u003e\n\u003cp\u003eAfter verification, two filters apply:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e--only-verified\u003c/code\u003e — drops all findings that are not \u003ccode\u003everified_active\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e--min-severity\u003c/code\u003e — drops findings below the specified severity level (\u003ccode\u003elow\u003c/code\u003e | \u003ccode\u003emedium\u003c/code\u003e | \u003ccode\u003ehigh\u003c/code\u003e | \u003ccode\u003ecritical\u003c/code\u003e; default \u003ccode\u003elow\u003c/code\u003e).\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eBoth filters run after verification so that verification status is available when \u003ccode\u003e--only-verified\u003c/code\u003e is evaluated.\u003c/p\u003e\n\u003ch2 id=\"9-output\"\u003e9. Output\u003c/h2\u003e\n\u003cp\u003eSurviving findings are passed to one of five \u003cstrong\u003eformatters\u003c/strong\u003e:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFormat\u003c/th\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eCommon use\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003eJSON\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e--format json\u003c/code\u003e (default)\u003c/td\u003e\n\u003ctd\u003eMachine-readable, pipeline-friendly\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eSARIF v2.1.0\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e--format sarif\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGitHub Code Scanning, security dashboards\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eCSV\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e--format csv\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSpreadsheets, data analysis\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eTable\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e--format table\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTerminal review, color-coded by severity\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eGitHub annotations\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e--format github\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInline pull-request annotations in GitHub Actions\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eOutput goes to stdout by default; use \u003ccode\u003e--output \u0026lt;file\u0026gt;\u003c/code\u003e to write to a file.\u003c/p\u003e\n\u003cp\u003eA \u003cstrong\u003escan summary\u003c/strong\u003e (date, source type, target, files scanned, duration, findings count, interrupted status) is always printed to \u003cstrong\u003estderr\u003c/strong\u003e after every scan, regardless of format or output destination.\u003c/p\u003e\n\u003ch2 id=\"secret-safety\"\u003eSecret safety\u003c/h2\u003e\n\u003cp\u003eLeakwatch is designed so that discovered secrets never leave the process boundary except for verification calls:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eRaw secret bytes live only in memory during detection and verification.\u003c/li\u003e\n\u003cli\u003eThe \u003ccode\u003e--show-raw\u003c/code\u003e flag is \u003ccode\u003efalse\u003c/code\u003e by default; without it, only the redacted representation appears in output.\u003c/li\u003e\n\u003cli\u003eSecrets are never written to disk, logged via \u003ccode\u003eslog\u003c/code\u003e, or cached between runs.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"design-decisions\"\u003eDesign decisions\u003c/h2\u003e\n\u003cp\u003eThe architecture reflects several deliberate choices documented as ADRs:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eGo + CGO disabled\u003c/strong\u003e (ADR-0001) — single static binary, no runtime dependencies, cross-compiles to all platforms.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCobra + Viper\u003c/strong\u003e (ADR-0002) — hierarchical CLI with \u003ccode\u003eflag \u0026gt; env \u0026gt; config \u0026gt; default\u003c/code\u003e precedence.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ego-git\u003c/strong\u003e (ADR-0003) — pure Go Git library; no external \u003ccode\u003egit\u003c/code\u003e binary required.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCompile-time detector registration\u003c/strong\u003e (ADR-0004) — \u003ccode\u003einit()\u003c/code\u003e + blank imports; type-safe, no runtime plugin loader.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAho-Corasick hybrid matching\u003c/strong\u003e (ADR-0005) — pre-filter eliminates most regex work on irrelevant chunks.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ego-containerregistry\u003c/strong\u003e (ADR-0006) — daemonless layer analysis; no Docker daemon required to scan images.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eWorker pool\u003c/strong\u003e (ADR-0008) — fixed goroutine count, channel-based fan-out; predictable memory and CPU usage.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eQuick Start\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eHow Verification Works\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eConfiguration File\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Reference\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/detectors/custom-rules\"\u003eCustom Rules\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n"},"getting-started/installation":{"title":"Installation","description":"Install Leakwatch via Homebrew, go install, Docker, or a prebuilt binary.","html":"\u003ch1 id=\"installation\"\u003eInstallation\u003c/h1\u003e\n\u003cp\u003eGetting Leakwatch onto your machine takes less than a minute. Choose the method that best fits your workflow: Homebrew is the simplest option on macOS and Linux, \u003ccode\u003ego install\u003c/code\u003e is ideal if you already have a Go toolchain, Docker keeps your host system clean, and prebuilt binaries work everywhere without any toolchain at all.\u003c/p\u003e\n\u003ch2 id=\"homebrew-macos-and-linux\"\u003eHomebrew (macOS and Linux)\u003c/h2\u003e\n\u003cp\u003eThe official tap supports macOS and Linux on both amd64 and arm64.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003ebrew install HodeTech/tap/leakwatch\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThe tap is hosted at \u003ca href=\"https://github.com/HodeTech/homebrew-tap\"\u003egithub.com/HodeTech/homebrew-tap\u003c/a\u003e. Homebrew handles upgrades with \u003ccode\u003ebrew upgrade leakwatch\u003c/code\u003e.\u003c/p\u003e\n\u003ch2 id=\"go-install\"\u003eGo install\u003c/h2\u003e\n\u003cp\u003eIf you have Go 1.25 or later installed, you can build and install the latest release directly from source:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003ego install github.com/HodeTech/leakwatch@latest\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThe binary is placed in \u003ccode\u003e$(go env GOPATH)/bin\u003c/code\u003e. Make sure that directory is on your \u003ccode\u003ePATH\u003c/code\u003e.\u003c/p\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNote\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003e\u003ccode\u003ego install\u003c/code\u003e always fetches the latest tagged release. To pin a specific version, replace \u003ccode\u003e@latest\u003c/code\u003e with the current release tag, \u003ccode\u003e@v1.8.0\u003c/code\u003e.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"docker\"\u003eDocker\u003c/h2\u003e\n\u003cp\u003eA minimal, multi-stage Alpine image is published to the GitHub Container Registry. The image runs as a non-root user (\u003ccode\u003eleakwatch\u003c/code\u003e), has CGO disabled, and uses \u003ccode\u003e/scan\u003c/code\u003e as its working directory.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003edocker run --rm \\\n -v \u0026quot;$(pwd):/scan\u0026quot; \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan fs /scan\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eAvailable tags:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eTag\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e:latest\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMost recent release\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e:v1.8.0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eExact current-release pin\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e:v1.7\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCurrent minor-version pin (tracks patch releases)\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eMount the directory you want to scan to \u003ccode\u003e/scan\u003c/code\u003e inside the container. Flags and options work identically to the native binary — see \u003ca href=\"#/reference/cli-reference\"\u003eCLI Reference\u003c/a\u003e for the full list.\u003c/p\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eTip\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eFor Docker-specific usage patterns, including scanning remote Git repositories and passing credentials securely, see \u003ca href=\"#/ci-cd/docker-usage\"\u003eUsing Docker\u003c/a\u003e.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"prebuilt-binary\"\u003ePrebuilt binary\u003c/h2\u003e\n\u003cp\u003eEvery release publishes tarballs for all supported platforms on the \u003ca href=\"https://github.com/HodeTech/Leakwatch/releases\"\u003eGitHub Releases\u003c/a\u003e page. Download the archive for your platform, extract it, and place the binary on your \u003ccode\u003ePATH\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSupported platforms:\u003c/strong\u003e Linux, macOS, and Windows on amd64 and arm64.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Example for Linux amd64 — replace OS and ARCH to match your platform\ncurl -LO https://github.com/HodeTech/Leakwatch/releases/latest/download/leakwatch_Linux_amd64.tar.gz\ntar -xzf leakwatch_Linux_amd64.tar.gz\nsudo mv leakwatch /usr/local/bin/leakwatch\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003ePlatform naming follows the pattern \u003ccode\u003eleakwatch_\u0026lt;OS\u0026gt;_\u0026lt;ARCH\u0026gt;.tar.gz\u003c/code\u003e where \u003ccode\u003e\u0026lt;OS\u0026gt;\u003c/code\u003e is \u003ccode\u003eLinux\u003c/code\u003e, \u003ccode\u003eDarwin\u003c/code\u003e, or \u003ccode\u003eWindows\u003c/code\u003e and \u003ccode\u003e\u0026lt;ARCH\u0026gt;\u003c/code\u003e is \u003ccode\u003eamd64\u003c/code\u003e or \u003ccode\u003earm64\u003c/code\u003e.\u003c/p\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eTip\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eEvery release also publishes a Software Bill of Materials (SBOM) for each archive and a \u003ca href=\"https://github.com/sigstore/cosign\"\u003eSigstore/cosign\u003c/a\u003e keyless signature (\u003ccode\u003echecksums.txt.sig\u003c/code\u003e / \u003ccode\u003echecksums.txt.pem\u003c/code\u003e) over the release's checksums file, so the provenance of a downloaded binary can be verified in supply-chain-sensitive environments.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"verifying-your-installation\"\u003eVerifying your installation\u003c/h2\u003e\n\u003cp\u003eAfter any installation method, confirm the binary is reachable and check the version:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch version\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eExpected output:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eleakwatch v1.8.0 (commit: 1a2b3c4, built: 2026-08-11T12:00:00Z)\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eIf the command is not found, check that the install directory is on your \u003ccode\u003ePATH\u003c/code\u003e.\u003c/p\u003e\n\u003ch2 id=\"next-steps\"\u003eNext steps\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eQuick Start\u003c/a\u003e — run your first scan in under a minute.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/how-it-works\"\u003eHow It Works\u003c/a\u003e — the architecture behind a Leakwatch scan.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eConfiguration File\u003c/a\u003e — customize scan behavior with \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eQuick Start\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/docker-usage\"\u003eUsing Docker\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Reference\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eConfiguration File\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n"},"getting-started/introduction":{"title":"Introduction","description":"What Leakwatch is, what it scans, and how it detects and verifies leaked secrets.","html":"\u003ch1 id=\"introduction\"\u003eIntroduction\u003c/h1\u003e\n\u003cp\u003e\u003cstrong\u003eLeakwatch\u003c/strong\u003e is a high-performance, open-source (MIT) security tool that \u003cstrong\u003edetects, verifies, and reports leaked secrets\u003c/strong\u003e — API keys, tokens, passwords, connection strings, and private keys — across your codebases, Git history, container images, cloud storage, and Slack workspaces.\u003c/p\u003e\n\u003cp\u003eIt is written in Go, ships as a single static binary with no runtime dependencies (\u003ccode\u003eCGO_ENABLED=0\u003c/code\u003e), and is built to run anywhere: a developer laptop, a pre-commit hook, or a CI/CD pipeline.\u003c/p\u003e\n\u003ch2 id=\"why-leakwatch\"\u003eWhy Leakwatch\u003c/h2\u003e\n\u003cp\u003eA leaked credential in a single commit — even one later deleted — can stay reachable in Git history forever and be exploited within minutes of being pushed. Leakwatch is designed to catch those secrets early and tell you which ones are \u003cem\u003eactually dangerous\u003c/em\u003e:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eBroad detection\u003c/strong\u003e — 65 built-in detectors covering cloud providers, AI APIs, payment platforms, databases, messaging tools, structured configuration, and more, plus your own YAML custom rules.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTruthful verification capability\u003c/strong\u003e — 39 detector types have a direct live check, 9 need trusted issuer/region/companion context, and 6 perform offline format validation. Registry count is never presented as live coverage.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMany sources\u003c/strong\u003e — scan a local filesystem, a full Git history, an OCI/Docker image, AWS S3, Google Cloud Storage, and Slack messages.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCI-native output\u003c/strong\u003e — JSON, SARIF (for GitHub Code Scanning), CSV, a colorized terminal table, and inline GitHub Actions annotations.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSecret-safe by design\u003c/strong\u003e — discovered secrets are redacted by default and are never logged, cached, or written to disk.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"what-it-scans\"\u003eWhat it scans\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eSource\u003c/th\u003e\n\u003cth\u003eCommand\u003c/th\u003e\n\u003cth\u003eWhat it covers\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003eFilesystem\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eleakwatch scan fs\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFiles in a local directory tree\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eGit history\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eleakwatch scan git\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eEvery blob across the full commit history (local or remote)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eContainer image\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eleakwatch scan image\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOCI/Docker image layers, daemonless\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eAWS S3\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eleakwatch scan s3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eObjects in an S3 bucket\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eGoogle Cloud Storage\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eleakwatch scan gcs\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eObjects in a GCS bucket\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eSlack\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eleakwatch scan slack\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMessage text in channels and (optionally) DMs\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eMultiple repos\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eleakwatch scan repos\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSeveral Git repositories at once\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"how-detection-works-briefly\"\u003eHow detection works, briefly\u003c/h2\u003e\n\u003cp\u003eLeakwatch uses a layered pipeline so it stays fast even on large inputs:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eAho-Corasick keyword pre-filter\u003c/strong\u003e — a single multi-pattern automaton quickly decides which detectors \u003cem\u003ecould\u003c/em\u003e match a chunk, so most detectors never run their regex.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRegex validation\u003c/strong\u003e — only the shortlisted detectors run their precise patterns.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eEntropy\u003c/strong\u003e — Shannon entropy is computed for display, and gates the built-in \u003ccode\u003egeneric-api-key\u003c/code\u003e detector plus any custom rule with its own entropy threshold, dropping low-randomness placeholder matches. Every other (structural) built-in detector is never gated by entropy.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eVerification\u003c/strong\u003e — eligible findings are checked against the live provider API.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eTip\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eYou don't have to understand the pipeline to use Leakwatch — but it explains why scans are fast and why some findings show a verification status while others don't. See \u003ca href=\"#/getting-started/how-it-works\"\u003eHow It Works\u003c/a\u003e for the full picture.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"what-leakwatch-is-not\"\u003eWhat Leakwatch is \u003cem\u003enot\u003c/em\u003e\u003c/h2\u003e\n\u003cp\u003eTo set expectations accurately:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eIt does \u003cstrong\u003enot\u003c/strong\u003e rewrite Git history or remove secrets for you — it finds and reports them, and (with \u003ccode\u003e--remediation\u003c/code\u003e) tells you how to rotate them.\u003c/li\u003e\n\u003cli\u003eSlack scans message text by default; bounded text-like attachments are opt-in with \u003ccode\u003e--include-files\u003c/code\u003e and the \u003ccode\u003efiles:read\u003c/code\u003e scope.\u003c/li\u003e\n\u003cli\u003eVerification is available for many but not all secret types — 11 detector types (such as JWTs and generic API keys) cannot be safely verified and are always reported as unverified.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"next-steps\"\u003eNext steps\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/installation\"\u003eInstallation\u003c/a\u003e — install via Homebrew, \u003ccode\u003ego install\u003c/code\u003e, Docker, or a prebuilt binary.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eQuick Start\u003c/a\u003e — run your first scan in under a minute.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/how-it-works\"\u003eHow It Works\u003c/a\u003e — the architecture behind the scan.\u003c/li\u003e\n\u003c/ul\u003e\n"},"getting-started/quick-start":{"title":"Quick Start","description":"Run your first Leakwatch scan in under a minute.","html":"\u003ch1 id=\"quick-start\"\u003eQuick Start\u003c/h1\u003e\n\u003cp\u003eThe fastest way to understand what Leakwatch can do is to point it at a real directory. This page walks you through your first scan, explains what the output means, and shows the flags you will reach for most often.\u003c/p\u003e\n\u003ch2 id=\"prerequisites\"\u003ePrerequisites\u003c/h2\u003e\n\u003cp\u003eLeakwatch must be installed and accessible on your \u003ccode\u003ePATH\u003c/code\u003e. If you have not done that yet, see \u003ca href=\"#/getting-started/installation\"\u003eInstallation\u003c/a\u003e.\u003c/p\u003e\n\u003ch2 id=\"your-first-scan\"\u003eYour first scan\u003c/h2\u003e\n\u003cp\u003eScan the current directory with one command:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs .\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBy default, output is JSON written to stdout. To get a human-readable, colorized table instead, add \u003ccode\u003e--format table\u003c/code\u003e:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eHere is what a result looks like (the table formatter always prints a trailing REMEDIATION column; it shows \u003ccode\u003e-\u003c/code\u003e unless \u003ccode\u003e--remediation\u003c/code\u003e is also set):\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eSEVERITY DETECTOR FILE LINE REDACTED STATUS REMEDIATION\n-------- -------- ---- ---- -------- ------ -----------\nCRITICAL aws-access-key-id config/deploy.env 12 ****MNOP verified_active -\nHIGH npm-token scripts/bootstrap.sh 37 npm_****wxyz verified_active -\nMEDIUM generic-api-key src/services/analytics.js 89 ****w2y4 unverified -\n\nFound 3 secrets (1 critical, 1 high, 1 medium).\n\n── Scan Summary ─────────────────────────────────\n Date: 2026-05-23 14:03:11\n Source: filesystem\n Target: /home/user/myproject\n Files scanned: 312\n Duration: 1.24s\n Findings: 3\n─────────────────────────────────────────────────\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThe scan summary is always printed to \u003cstrong\u003estderr\u003c/strong\u003e, so it never interferes with piped or redirected output.\u003c/p\u003e\n\u003ch2 id=\"understanding-a-finding\"\u003eUnderstanding a finding\u003c/h2\u003e\n\u003cp\u003eEach row in the table (or object in JSON) represents one finding. The key fields are:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eField\u003c/th\u003e\n\u003cth\u003eMeaning\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eSEVERITY\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003eHow critical the secret type is: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e, or \u003ccode\u003ecritical\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eDETECTOR\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003eThe detector that matched — identifies the secret type (e.g. \u003ccode\u003eaws-access-key-id\u003c/code\u003e)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eFILE\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003ePath to the file where the secret was found, relative to the scan root\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eLINE\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003eLine number of the match\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eREDACTED\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003eA masked representation of the secret — never the raw value unless \u003ccode\u003e--show-raw\u003c/code\u003e is set\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eSTATUS\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003eVerification outcome: \u003ccode\u003everified_active\u003c/code\u003e, \u003ccode\u003everified_inactive\u003c/code\u003e, \u003ccode\u003eunverified\u003c/code\u003e, or \u003ccode\u003everify_error\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eREMEDIATION\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003eRotation/revocation guidance title, or \u003ccode\u003e-\u003c/code\u003e when \u003ccode\u003e--remediation\u003c/code\u003e was not passed\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eA \u003ccode\u003everified_active\u003c/code\u003e status means Leakwatch confirmed the secret is still live with a contract-valid, non-destructive provider check. \u003cstrong\u003eTreat every \u003ccode\u003everified_active\u003c/code\u003e finding as an open incident.\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"common-scan-options\"\u003eCommon scan options\u003c/h2\u003e\n\u003ch3 id=\"focus-on-confirmed-secrets-only\"\u003eFocus on confirmed secrets only\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --only-verified\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThis hides unverified and inactive findings, leaving only those confirmed live. Useful for triage when you have many results.\u003c/p\u003e\n\u003ch3 id=\"skip-network-verification-for-a-fast-offline-scan\"\u003eSkip network verification for a fast offline scan\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --no-verify\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eVerification is skipped entirely — no outbound network calls are made. Results appear faster and work without internet access, but all findings are marked \u003ccode\u003eunverified\u003c/code\u003e.\u003c/p\u003e\n\u003ch3 id=\"add-remediation-guidance\"\u003eAdd remediation guidance\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --remediation --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eEach finding gains a \u003cstrong\u003eREMEDIATION\u003c/strong\u003e column explaining how to rotate or revoke the specific secret type. The same data is included in JSON, SARIF, and CSV output when the flag is set.\u003c/p\u003e\n\u003ch3 id=\"filter-by-minimum-severity\"\u003eFilter by minimum severity\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --min-severity high\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eOnly findings at \u003ccode\u003ehigh\u003c/code\u003e or \u003ccode\u003ecritical\u003c/code\u003e severity are reported.\u003c/p\u003e\n\u003ch3 id=\"save-results-to-a-file\"\u003eSave results to a file\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --format sarif --output results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThe \u003ccode\u003e--output\u003c/code\u003e / \u003ccode\u003e-o\u003c/code\u003e flag writes to a file instead of stdout. SARIF output is compatible with \u003ca href=\"https://docs.github.com/en/code-security/code-scanning\"\u003eGitHub Code Scanning\u003c/a\u003e.\u003c/p\u003e\n\u003ch2 id=\"generate-a-configuration-file\"\u003eGenerate a configuration file\u003c/h2\u003e\n\u003cp\u003eRunning Leakwatch with defaults is fine for a first try, but for repeated use you will want a project-level configuration:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch init\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThis writes \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e in the current directory with recommended defaults for concurrency, entropy, verification, output format, and common path exclusions. Use \u003ccode\u003e--force\u003c/code\u003e to overwrite an existing file, or \u003ccode\u003e--output\u003c/code\u003e to write to a different path.\u003c/p\u003e\n\u003cp\u003eSee \u003ca href=\"#/configuration/config-file\"\u003eConfiguration File\u003c/a\u003e for a full explanation of every option.\u003c/p\u003e\n\u003ch2 id=\"exit-codes\"\u003eExit codes\u003c/h2\u003e\n\u003cp\u003eLeakwatch uses distinct exit codes so CI scripts can act on results without parsing output:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eCode\u003c/th\u003e\n\u003cth\u003eMeaning\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan completed — no findings\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan completed — one or more secrets found\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan failed due to an error\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan was interrupted (\u003ccode\u003eCtrl+C\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e) before it completed, and no findings had been reported yet\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eA typical CI gate looks like:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --only-verified --format sarif --output results.sarif\nif [ $? -eq 1 ]; then\n echo \u0026quot;Active secrets found — failing build\u0026quot;\n exit 1\nfi\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-warn\"\u003e\u003cdiv class=\"callout-label\"\u003eWarning\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eExit code \u003ccode\u003e1\u003c/code\u003e is returned whenever \u003cem\u003eany\u003c/em\u003e finding passes the active filters (including \u003ccode\u003e--min-severity\u003c/code\u003e and \u003ccode\u003e--only-verified\u003c/code\u003e). A clean exit code \u003ccode\u003e0\u003c/code\u003e means no findings matched — not that no secrets exist in the codebase.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"cancelling-a-scan\"\u003eCancelling a scan\u003c/h2\u003e\n\u003cp\u003ePress \u003ccode\u003eCtrl+C\u003c/code\u003e (or send \u003ccode\u003eSIGTERM\u003c/code\u003e) to cancel a running scan. Leakwatch stops gracefully: in-flight chunks finish, partial results are written, and the summary indicates \u003ccode\u003eStatus: interrupted (partial results)\u003c/code\u003e. Findings still take precedence over interruption — if secrets were already found before the interrupt, the scan exits \u003ccode\u003e1\u003c/code\u003e so that signal is never masked. Exit code \u003ccode\u003e3\u003c/code\u003e is returned only when the scan was interrupted with zero findings reported, so a CI pipeline can never mistake a scan that did not finish for a clean pass.\u003c/p\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/installation\"\u003eInstallation\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/how-it-works\"\u003eHow It Works\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Reference\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eConfiguration File\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eHow Verification Works\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n"},"output/output-formats":{"title":"Output Formats","description":"The five output formats Leakwatch supports — JSON, SARIF, CSV, table, and GitHub annotations — with examples and guidance on when to use each.","html":"\u003ch1 id=\"output-formats\"\u003eOutput Formats\u003c/h1\u003e\n\u003cp\u003eLeakwatch supports five output formats, covering machine-readable pipelines, security tooling integrations, spreadsheet exports, human-readable terminal review, and GitHub Actions annotations. Select a format with \u003ccode\u003e--format\u003c/code\u003e (or \u003ccode\u003e-f\u003c/code\u003e); write to a file instead of stdout with \u003ccode\u003e--output\u003c/code\u003e (or \u003ccode\u003e-o\u003c/code\u003e).\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --format json\nleakwatch scan fs . --format sarif --output results.sarif\nleakwatch scan fs . --format csv --output findings.csv\nleakwatch scan fs . --format table\nleakwatch scan fs . --format github # GitHub Actions annotations (CI use)\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThe default format is \u003ccode\u003ejson\u003c/code\u003e.\u003c/p\u003e\n\u003ch2 id=\"json\"\u003eJSON\u003c/h2\u003e\n\u003cp\u003eJSON is the default format and the most complete representation. Leakwatch writes a JSON \u003cstrong\u003earray\u003c/strong\u003e of finding objects to stdout (or to the file given by \u003ccode\u003e--output\u003c/code\u003e).\u003c/p\u003e\n\u003cp\u003eThe raw secret value is \u003cstrong\u003enever\u003c/strong\u003e serialized unless \u003ccode\u003e--show-raw\u003c/code\u003e is explicitly set. With \u003ccode\u003e--show-raw\u003c/code\u003e, a \u003ccode\u003e\u0026quot;raw\u0026quot;\u003c/code\u003e field is added to each object.\u003c/p\u003e\n\u003ch3 id=\"example-invocation\"\u003eExample invocation\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs ./src --format json --output findings.json\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"example-finding-object\"\u003eExample finding object\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-json\"\u003e{\n \u0026quot;id\u0026quot;: \u0026quot;447b5d2846d08ce25dd3d638cfe911ad\u0026quot;,\n \u0026quot;detector_id\u0026quot;: \u0026quot;github-token\u0026quot;,\n \u0026quot;severity\u0026quot;: \u0026quot;critical\u0026quot;,\n \u0026quot;redacted\u0026quot;: \u0026quot;ghp_****************************Xk9R\u0026quot;,\n \u0026quot;source\u0026quot;: {\n \u0026quot;source_type\u0026quot;: \u0026quot;filesystem\u0026quot;,\n \u0026quot;file_path\u0026quot;: \u0026quot;scripts/deploy.sh\u0026quot;,\n \u0026quot;line\u0026quot;: 14\n },\n \u0026quot;verification\u0026quot;: {\n \u0026quot;status\u0026quot;: \u0026quot;verified_active\u0026quot;\n },\n \u0026quot;entropy\u0026quot;: 5.82,\n \u0026quot;detected_at\u0026quot;: \u0026quot;2026-05-23T10:15:30Z\u0026quot;\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003eid\u003c/code\u003e is a deterministic, truncated SHA-256 rendered as a flat 32-character lowercase hex string — it is \u003cstrong\u003enot\u003c/strong\u003e a UUID and never contains dashes. See \u003ca href=\"#/getting-started/how-it-works\"\u003eHow It Works\u003c/a\u003e for how it is computed.\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eentropy\u003c/code\u003e is omitted when it was not calculated; a calculated value of \u003ccode\u003e0.0\u003c/code\u003e is emitted as zero. \u003ccode\u003edetected_at\u003c/code\u003e is omitted for library-created findings that have no timestamp instead of emitting Go's year-1 zero time. Normal CLI findings set the timestamp.\u003c/p\u003e\n\u003cp\u003eProvider-validated identity, scope, and expiry metadata appears under \u003ccode\u003everification.extra_data\u003c/code\u003e when available. Values are bounded and schema-validated; absence means the provider did not supply safe metadata, not that the token has no permissions or expiry.\u003c/p\u003e\n\u003cp\u003eWhen \u003ccode\u003e--remediation\u003c/code\u003e is also set, a \u003ccode\u003e\u0026quot;remediation\u0026quot;\u003c/code\u003e object is nested inside each finding. See \u003ca href=\"#/output/remediation\"\u003eRemediation Guidance\u003c/a\u003e.\u003c/p\u003e\n\u003ch2 id=\"sarif\"\u003eSARIF\u003c/h2\u003e\n\u003cp\u003eThe \u003ccode\u003esarif\u003c/code\u003e format produces a SARIF v2.1.0 document, designed for upload to \u003ca href=\"https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/uploading-a-sarif-file-to-github\"\u003eGitHub Code Scanning\u003c/a\u003e. The tool name is \u003ccode\u003eLeakwatch\u003c/code\u003e and \u003ccode\u003einformationUri\u003c/code\u003e points to \u003ccode\u003ehttps://github.com/HodeTech/Leakwatch\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003eEach detector that appears in the findings becomes a \u003cstrong\u003erule\u003c/strong\u003e in the SARIF driver, complete with \u003ccode\u003ehelp\u003c/code\u003e text (populated from remediation steps when \u003ccode\u003e--remediation\u003c/code\u003e is set) and a \u003ccode\u003ehelpUri\u003c/code\u003e pointing to the provider documentation. Results carry a \u003ccode\u003eleakwatch/v1\u003c/code\u003e partial fingerprint computed from the detector ID, redacted value, and file path — this lets GitHub Code Scanning track the same alert even when surrounding code shifts.\u003c/p\u003e\n\u003ch3 id=\"example-invocation-1\"\u003eExample invocation\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --format sarif --output results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"uploading-to-github-code-scanning\"\u003eUploading to GitHub Code Scanning\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e# In a GitHub Actions workflow step:\n- name: Upload SARIF results\n uses: github/codeql-action/upload-sarif@v3\n with:\n sarif_file: results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eSee \u003ca href=\"#/ci-cd/github-action\"\u003eGitHub Action\u003c/a\u003e for the full CI setup.\u003c/p\u003e\n\u003ch2 id=\"csv\"\u003eCSV\u003c/h2\u003e\n\u003cp\u003eThe \u003ccode\u003ecsv\u003c/code\u003e format writes a header row followed by one row per finding, using standard comma-separated values. Every cell is sanitized against spreadsheet formula injection before writing.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eColumns (default):\u003c/strong\u003e\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eid,detector_id,severity,redacted,source,file_path,line,commit,verification_status,remediation\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003esource\u003c/code\u003e is a human-readable label identifying the origin repository, container image, or Slack channel a finding came from (useful when a \u003ccode\u003escan repos\u003c/code\u003e run combines findings from several repositories into one CSV); it is empty for a plain filesystem scan of a single directory. \u003ccode\u003eline\u003c/code\u003e is the line number, empty when not applicable.\u003c/p\u003e\n\u003cp\u003eWhen \u003ccode\u003e--show-raw\u003c/code\u003e is set, a trailing \u003ccode\u003eraw\u003c/code\u003e column is appended.\u003c/p\u003e\n\u003cp\u003eThe \u003ccode\u003eremediation\u003c/code\u003e column contains the remediation title (e.g. \u003ccode\u003e\u0026quot;Revoke GitHub Token\u0026quot;\u003c/code\u003e) when \u003ccode\u003e--remediation\u003c/code\u003e is set, and is empty otherwise.\u003c/p\u003e\n\u003ch3 id=\"example-invocation-2\"\u003eExample invocation\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git . --format csv --output findings.csv\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"example-output\"\u003eExample output\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-csv\"\u003eid,detector_id,severity,redacted,source,file_path,line,commit,verification_status,remediation\n447b5d2846d08ce25dd3d638cfe911ad,github-token,critical,ghp_****Xk9R,,scripts/deploy.sh,14,7d3e1f2,verified_active,Revoke GitHub Token\ne6fa909746d7d5242309b64c33209fa9,aws-access-key-id,high,AKIA****K7NP,,config/aws.yml,3,7d3e1f2,unverified,Rotate AWS Access Key\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"table\"\u003eTable\u003c/h2\u003e\n\u003cp\u003eThe \u003ccode\u003etable\u003c/code\u003e format writes a human-readable terminal-cell-aligned table, including correct display width for CJK, combining marks, and emoji. It is best suited for interactive terminal sessions where you want a quick visual scan of the results.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eColumns:\u003c/strong\u003e\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eSEVERITY | DETECTOR | FILE | LINE | REDACTED | STATUS | REMEDIATION\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThe \u003ccode\u003eLINE\u003c/code\u003e column shows \u003ccode\u003e-\u003c/code\u003e when no line number is available (for example a Slack or container-image finding). The \u003ccode\u003eREMEDIATION\u003c/code\u003e column is always present and shows \u003ccode\u003e-\u003c/code\u003e unless \u003ccode\u003e--remediation\u003c/code\u003e is set. When \u003ccode\u003e--show-raw\u003c/code\u003e is also set, a trailing \u003ccode\u003eRAW\u003c/code\u003e column is appended. In table output only, this value is a reversible Go-quoted string: quotes and escapes such as \u003ccode\u003e\\\\n\u003c/code\u003e, \u003ccode\u003e\\\\t\u003c/code\u003e, and \u003ccode\u003e\\\\x1b\u003c/code\u003e keep terminal control bytes inert while \u003ccode\u003estrconv.Unquote\u003c/code\u003e can recover the exact original bytes. JSON and CSV raw values retain their existing machine-readable representation. A summary line is printed at the bottom of the table (e.g. \u003ccode\u003eFound 3 secrets (1 critical, 2 high).\u003c/code\u003e).\u003c/p\u003e\n\u003cp\u003eAttacker-influenced fields (detector ID, file path, redacted value) are stripped of control characters and ANSI escape sequences before being written to the terminal. Unicode bidi controls are rendered as visible \u003ccode\u003e\\\\uXXXX\u003c/code\u003e code points, preventing visual reordering without hiding their presence. Ordinary internationalized text, including CJK, combining marks, and emoji ZWJ sequences, is preserved.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eANSI color\u003c/strong\u003e is applied to the \u003ccode\u003eSEVERITY\u003c/code\u003e column automatically, but only when all four conditions are met:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003ccode\u003e--format table\u003c/code\u003e is selected\u003c/li\u003e\n\u003cli\u003eOutput goes to stdout (no \u003ccode\u003e--output \u0026lt;file\u0026gt;\u003c/code\u003e)\u003c/li\u003e\n\u003cli\u003estdout is a TTY (not a pipe or redirect)\u003c/li\u003e\n\u003cli\u003eThe \u003ccode\u003eNO_COLOR\u003c/code\u003e environment variable is unset\u003c/li\u003e\n\u003c/ol\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eSeverity\u003c/th\u003e\n\u003cth\u003eColor\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecritical\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBold red\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ehigh\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRed\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003emedium\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYellow\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBlue\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"example-invocation-3\"\u003eExample invocation\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --format table --min-severity high\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"example-output-1\"\u003eExample output\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eSEVERITY DETECTOR FILE LINE REDACTED STATUS REMEDIATION\n-------- -------- ---- ---- -------- ------ -----------\nCRITICAL github-token scripts/deploy.sh 14 ghp_****Xk9R verified_active Revoke GitHub Token\nHIGH aws-access-key-id config/aws.yml 3 AKIA****K7NP unverified Rotate AWS Access Key\n\nFound 2 secrets (1 critical, 1 high).\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"github-annotations\"\u003eGitHub annotations\u003c/h2\u003e\n\u003cp\u003eThe \u003ccode\u003egithub\u003c/code\u003e format emits \u003ca href=\"https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions\"\u003eGitHub Actions workflow commands\u003c/a\u003e (\u003ccode\u003e::error\u003c/code\u003e / \u003ccode\u003e::warning\u003c/code\u003e / \u003ccode\u003e::notice\u003c/code\u003e) so findings appear as \u003cstrong\u003einline annotations\u003c/strong\u003e on a pull request's \u003cem\u003eFiles changed\u003c/em\u003e view and in the run log. It is intended to be streamed to the runner's stdout — writing it to a file has no effect.\u003c/p\u003e\n\u003cp\u003eSeverity maps to the annotation level: \u003ccode\u003ecritical\u003c/code\u003e → \u003ccode\u003eerror\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e → \u003ccode\u003ewarning\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e/\u003ccode\u003elow\u003c/code\u003e → \u003ccode\u003enotice\u003c/code\u003e. A finding with a file path is anchored to that file and line; a finding without one becomes a run-level annotation.\u003c/p\u003e\n\u003cp\u003eFor safety, this format \u003cstrong\u003enever\u003c/strong\u003e prints the raw secret — only the redacted value is shown, even with \u003ccode\u003e--show-raw\u003c/code\u003e, because annotations render in the (often public) PR UI and logs.\u003c/p\u003e\n\u003ch3 id=\"example-invocation-4\"\u003eExample invocation\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --format github\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"example-output-2\"\u003eExample output\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003e::error file=config/prod.env,line=12,title=Leakwatch%3A aws-access-key-id::Potential secret detected by aws-access-key-id (critical): AKIA****K7NP\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThis format is normally driven by the \u003ca href=\"#/ci-cd/github-action\"\u003eGitHub Action\u003c/a\u003e (\u003ccode\u003eformat: github\u003c/code\u003e) rather than invoked by hand.\u003c/p\u003e\n\u003ch2 id=\"common-output-flags\"\u003eCommon output flags\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eShort\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-f\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOutput format: \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, \u003ccode\u003egithub\u003c/code\u003e (default \u003ccode\u003ejson\u003c/code\u003e)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-o\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eWrite to file instead of stdout\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--show-raw\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003c/td\u003e\n\u003ctd\u003eInclude unredacted secret value in output\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003c/td\u003e\n\u003ctd\u003eDrop findings below this severity level\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003c/td\u003e\n\u003ctd\u003eKeep only \u003ccode\u003everified_active\u003c/code\u003e findings\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--remediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003c/td\u003e\n\u003ctd\u003eEnrich findings with provider remediation guidance\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/output/remediation\"\u003eRemediation Guidance\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/github-action\"\u003eGitHub Action\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eHow Verification Works\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n"},"output/remediation":{"title":"Remediation Guidance","description":"Use --remediation to enrich findings with provider-specific rotation and revocation steps, urgency ratings, and official documentation links.","html":"\u003ch1 id=\"remediation-guidance\"\u003eRemediation Guidance\u003c/h1\u003e\n\u003cp\u003eKnowing a secret is leaked is only half the work — you also need to know what to do about it. Passing \u003ccode\u003e--remediation\u003c/code\u003e to any scan command enriches each finding with structured, provider-specific guidance: the steps to rotate or revoke the credential, a link to the provider's documentation, a link to the management console, an urgency rating, and a verification checklist.\u003c/p\u003e\n\u003ch2 id=\"how-to-enable-it\"\u003eHow to enable it\u003c/h2\u003e\n\u003cp\u003eAdd \u003ccode\u003e--remediation\u003c/code\u003e to any scan command:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --remediation\nleakwatch scan git . --remediation --format json\nleakwatch scan image myapp:latest --remediation --format sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eRemediation enrichment is disabled by default. When the flag is absent, the \u003ccode\u003eremediation\u003c/code\u003e field in each finding is \u003ccode\u003enull\u003c/code\u003e and no extra data is fetched or computed.\u003c/p\u003e\n\u003ch2 id=\"what-it-contains\"\u003eWhat it contains\u003c/h2\u003e\n\u003cp\u003eEach remediation entry includes the following fields:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eField\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003etitle\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eShort name of the remediation action (e.g. \u003ccode\u003e\u0026quot;Rotate AWS Access Key\u0026quot;\u003c/code\u003e)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esteps\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOrdered list of steps to rotate or revoke the secret\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edoc_url\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eLink to the provider's official credential-management documentation\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003econsole_url\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDirect link to the provider's management console page\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eurgency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHow quickly to act: \u003ccode\u003e\u0026quot;immediate\u0026quot;\u003c/code\u003e, \u003ccode\u003e\u0026quot;high\u0026quot;\u003c/code\u003e, or \u003ccode\u003e\u0026quot;medium\u0026quot;\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003echecklist\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePost-rotation verification steps (e.g. review audit logs, notify the security team)\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eLeakwatch ships 64 remediation entries — one for every built-in detector. All 64 entries are included in the binary; no network calls are made to fetch guidance.\u003c/p\u003e\n\u003ch2 id=\"how-it-appears-in-each-format\"\u003eHow it appears in each format\u003c/h2\u003e\n\u003cp\u003eEnrichment adds the guidance to the finding object in memory. How it surfaces depends on the output format:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eJSON\u003c/strong\u003e — the full structured \u003ccode\u003eremediation\u003c/code\u003e object is nested inside each finding:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --remediation --format json\n\u003c/code\u003e\u003c/pre\u003e\n\u003cpre\u003e\u003ccode class=\"language-json\"\u003e{\n \u0026quot;id\u0026quot;: \u0026quot;447b5d2846d08ce25dd3d638cfe911ad\u0026quot;,\n \u0026quot;detector_id\u0026quot;: \u0026quot;github-token\u0026quot;,\n \u0026quot;severity\u0026quot;: \u0026quot;critical\u0026quot;,\n \u0026quot;redacted\u0026quot;: \u0026quot;ghp_****************************Xk9R\u0026quot;,\n \u0026quot;source\u0026quot;: {\n \u0026quot;source_type\u0026quot;: \u0026quot;filesystem\u0026quot;,\n \u0026quot;file_path\u0026quot;: \u0026quot;scripts/deploy.sh\u0026quot;,\n \u0026quot;line\u0026quot;: 14\n },\n \u0026quot;verification\u0026quot;: {\n \u0026quot;status\u0026quot;: \u0026quot;verified_active\u0026quot;\n },\n \u0026quot;remediation\u0026quot;: {\n \u0026quot;title\u0026quot;: \u0026quot;Revoke GitHub Token\u0026quot;,\n \u0026quot;steps\u0026quot;: [\n \u0026quot;Go to GitHub Settings \u0026gt; Developer settings \u0026gt; Personal access tokens.\u0026quot;,\n \u0026quot;Revoke the compromised token immediately.\u0026quot;,\n \u0026quot;Create a new token with the minimum required scopes.\u0026quot;,\n \u0026quot;Update all integrations and CI/CD pipelines with the new token.\u0026quot;\n ],\n \u0026quot;doc_url\u0026quot;: \u0026quot;https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens\u0026quot;,\n \u0026quot;console_url\u0026quot;: \u0026quot;https://github.com/settings/tokens\u0026quot;,\n \u0026quot;urgency\u0026quot;: \u0026quot;immediate\u0026quot;,\n \u0026quot;checklist\u0026quot;: [\n \u0026quot;Review the GitHub audit log for unauthorized actions performed with the token.\u0026quot;,\n \u0026quot;Check repository and organization settings for unexpected changes.\u0026quot;,\n \u0026quot;Notify the security team about the exposure.\u0026quot;,\n \u0026quot;Scan for other repositories that may contain the same token.\u0026quot;\n ]\n },\n \u0026quot;entropy\u0026quot;: 5.82,\n \u0026quot;detected_at\u0026quot;: \u0026quot;2026-05-23T10:15:30Z\u0026quot;\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003cstrong\u003eSARIF\u003c/strong\u003e — the \u003ccode\u003esteps\u003c/code\u003e are embedded in the rule's \u003ccode\u003ehelp.text\u003c/code\u003e field, and \u003ccode\u003edoc_url\u003c/code\u003e is set as the rule's \u003ccode\u003ehelpUri\u003c/code\u003e. This surfaces directly in GitHub Code Scanning's alert details panel.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eCSV\u003c/strong\u003e — only the remediation \u003ccode\u003etitle\u003c/code\u003e is written to the \u003ccode\u003eremediation\u003c/code\u003e column. The full structured guidance is not included in the CSV output.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTable\u003c/strong\u003e — only the remediation \u003ccode\u003etitle\u003c/code\u003e is shown in the \u003ccode\u003eREMEDIATION\u003c/code\u003e column.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --remediation --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eSEVERITY DETECTOR FILE LINE REDACTED STATUS REMEDIATION\n-------- -------- ---- ---- -------- ------ -----------\nCRITICAL github-token scripts/deploy.sh 14 ghp_****Xk9R verified_active Revoke GitHub Token\n\nFound 1 secret (1 critical).\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eTip\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eUse \u003ccode\u003e--remediation --format json\u003c/code\u003e when you need the full structured guidance for automated incident-response workflows. Use \u003ccode\u003e--remediation --format table\u003c/code\u003e for a quick human-readable triage session in the terminal.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNote\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eEnrichment runs only when \u003ccode\u003e--remediation\u003c/code\u003e is set. Without the flag, the \u003ccode\u003eremediation\u003c/code\u003e field is absent from JSON and SARIF output, the CSV \u003ccode\u003eremediation\u003c/code\u003e column is empty, and the table \u003ccode\u003eREMEDIATION\u003c/code\u003e column shows \u003ccode\u003e-\u003c/code\u003e. The flag does not modify the original scan results — it adds a layer on top.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/output/output-formats\"\u003eOutput Formats\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eHow Verification Works\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n"},"reference/cli-reference":{"title":"CLI Reference","description":"Complete reference for every Leakwatch command, subcommand, and flag.","html":"\u003ch1 id=\"cli-reference\"\u003eCLI Reference\u003c/h1\u003e\n\u003cp\u003eThis page is the authoritative reference for all Leakwatch commands and flags. For conceptual explanations and worked examples, follow the cross-links to the relevant scanning or configuration pages.\u003c/p\u003e\n\u003ch2 id=\"global-flags\"\u003eGlobal flags\u003c/h2\u003e\n\u003cp\u003eThese flags are available on every command and subcommand.\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--config \u0026lt;path\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eauto-discovered \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePath to a configuration file. When omitted, Leakwatch searches the current directory, then the user's home directory, for \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e — there is \u003cstrong\u003eno\u003c/strong\u003e parent-directory walk. A malformed config file (invalid YAML) at either location is a fatal error.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--log-level \u0026lt;level\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ewarn\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eLogging verbosity: \u003ccode\u003edebug\u003c/code\u003e, \u003ccode\u003einfo\u003c/code\u003e, \u003ccode\u003ewarn\u003c/code\u003e, or \u003ccode\u003eerror\u003c/code\u003e. Log output goes to stderr and does not affect scan results.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"leakwatch-version\"\u003e\u003ccode\u003eleakwatch version\u003c/code\u003e\u003c/h2\u003e\n\u003cp\u003ePrints the binary version, commit hash, and build timestamp, then exits.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch version\n\u003c/code\u003e\u003c/pre\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eleakwatch v1.8.0 (commit: 1a2b3c4, built: 2026-08-11T12:00:00Z)\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"leakwatch-init\"\u003e\u003ccode\u003eleakwatch init\u003c/code\u003e\u003c/h2\u003e\n\u003cp\u003eGenerates a \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e configuration file in the current directory with recommended defaults.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch init [flags]\n\u003c/code\u003e\u003c/pre\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output \u0026lt;path\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e.leakwatch.yaml\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eWrite the config file to this path instead of the default.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--force\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOverwrite an existing config file. Without this flag, \u003ccode\u003einit\u003c/code\u003e exits with an error if the output file already exists.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Generate the default config\nleakwatch init\n\n# Overwrite an existing config\nleakwatch init --force\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"leakwatch-scan\"\u003e\u003ccode\u003eleakwatch scan\u003c/code\u003e\u003c/h2\u003e\n\u003cp\u003eParent command for all scan subcommands. Has no behavior on its own; run a subcommand.\u003c/p\u003e\n\u003ch3 id=\"common-scan-flags\"\u003eCommon scan flags\u003c/h3\u003e\n\u003cp\u003eThe following flags are available on \u003cstrong\u003eall\u003c/strong\u003e \u003ccode\u003escan\u003c/code\u003e subcommands.\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eShort\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-f\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ejson\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOutput format: \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, or \u003ccode\u003egithub\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-o\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estdout\u003c/td\u003e\n\u003ctd\u003eWrite results to this file path instead of stdout. A bare path with no extension is auto-suffixed with the format's own extension (e.g. \u003ccode\u003e--format sarif --output results\u003c/code\u003e writes \u003ccode\u003eresults.sarif\u003c/code\u003e). The file is written with \u003ccode\u003e0600\u003c/code\u003e permissions. Ignored for \u003ccode\u003e--format github\u003c/code\u003e, which always writes to stdout.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-c\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCPU count\u003c/td\u003e\n\u003ctd\u003eNumber of concurrent scan workers.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--max-file-size\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e10485760\u003c/code\u003e (10 MB)\u003c/td\u003e\n\u003ctd\u003eSkip files or blobs larger than this number of bytes.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--show-raw\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInclude the raw (unredacted) secret value in output. Use with caution.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-detectors\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eDetector IDs to exclude for this run (e.g. \u003ccode\u003eaws-access-key-id,generic-api-key\u003c/code\u003e). Repeatable or comma-separated; combined with (not a replacement for) \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e in the config file.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--no-verify\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDisable live secret verification. No outbound API calls are made.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eReport only findings that Leakwatch has confirmed are active via live verification.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--verifier-origin \u0026lt;id=url\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eBind a context-required detector to an operator-trusted HTTPS origin. Repeatable; accepted only on the command line.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--grafana-instance-url \u0026lt;url\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eBackward-compatible alias that supplies the trusted Grafana origin.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMinimum severity to include in output: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e, or \u003ccode\u003ecritical\u003c/code\u003e. An unrecognized value (e.g. a typo) is a hard error rather than being silently accepted.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--remediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAttach remediation guidance (rotation/revocation steps) to each finding.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e (repeatable) is also available on every scan subcommand \u003cstrong\u003eexcept\u003c/strong\u003e \u003ccode\u003escan slack\u003c/code\u003e — see each subcommand's own flag table below, since it is registered per-command rather than shared by every subcommand.\u003c/p\u003e\n\u003cp\u003eSee \u003ca href=\"#/reference/exit-codes\"\u003eExit Codes\u003c/a\u003e for the full exit-code contract: \u003ccode\u003e0\u003c/code\u003e clean, \u003ccode\u003e1\u003c/code\u003e findings, \u003ccode\u003e2\u003c/code\u003e error, \u003ccode\u003e3\u003c/code\u003e interrupted before completion.\u003c/p\u003e\n\u003chr\u003e\n\u003ch3 id=\"scan-fs\"\u003e\u003ccode\u003escan fs\u003c/code\u003e\u003c/h3\u003e\n\u003cp\u003eScans one or more local filesystem paths — each may be a directory (walked recursively) or a single file.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs [path...] [flags]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eAccepts \u003cstrong\u003ezero or more\u003c/strong\u003e positional arguments. With no path given, \u003ccode\u003e.\u003c/code\u003e (the current directory) is scanned. Multiple paths may be mixed files and directories in one invocation.\u003c/p\u003e\n\u003ch4 id=\"filesystem-specific-flags\"\u003eFilesystem-specific flags\u003c/h4\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eGlob pattern for paths to exclude. Repeatable.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch4 id=\"examples\"\u003eExamples\u003c/h4\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Scan the current directory, print a colorized table\nleakwatch scan fs . --format table\n\n# Scan a single file\nleakwatch scan fs cmd/main.go\n\n# Scan several files and directories at once\nleakwatch scan fs cmd/ main.go internal/config\n\n# Save SARIF output, exclude test files and vendor\nleakwatch scan fs . \\\n --exclude \u0026quot;**/*_test.go\u0026quot; \\\n --exclude \u0026quot;vendor/**\u0026quot; \\\n --format sarif \\\n --output results.sarif\n\n# Exclude specific detectors for this run\nleakwatch scan fs . --exclude-detectors aws-access-key-id,generic-api-key\n\u003c/code\u003e\u003c/pre\u003e\n\u003chr\u003e\n\u003ch3 id=\"scan-git\"\u003e\u003ccode\u003escan git\u003c/code\u003e\u003c/h3\u003e\n\u003cp\u003eScans the full commit history of a local or remote Git repository.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git \u0026lt;url_or_path\u0026gt; [flags]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eExactly one positional argument is required: a local path or an HTTP/HTTPS/SSH URL.\u003c/p\u003e\n\u003ch4 id=\"git-specific-flags\"\u003eGit-specific flags\u003c/h4\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--since \u0026lt;YYYY-MM-DD\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eScan only commits after this date.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--since-commit \u0026lt;hash\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eScan only changes from this commit hash to HEAD.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--branch \u0026lt;name\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eTarget a specific branch instead of the default branch.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--depth \u0026lt;int\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e (full)\u003c/td\u003e\n\u003ctd\u003eShallow clone depth for remote repositories. \u003ccode\u003e0\u003c/code\u003e fetches the full history.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eGlob pattern for blob paths to exclude. Repeatable.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch4 id=\"examples-1\"\u003eExamples\u003c/h4\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Scan full local history\nleakwatch scan git . --format table\n\n# Scan only commits added by a pull request\nleakwatch scan git . --since-commit a1b2c3d --format json\n\u003c/code\u003e\u003c/pre\u003e\n\u003chr\u003e\n\u003ch3 id=\"scan-image\"\u003e\u003ccode\u003escan image\u003c/code\u003e\u003c/h3\u003e\n\u003cp\u003eScans the layers of an OCI/Docker image for secrets. Leakwatch is daemonless and pulls directly from the registry — no Docker socket is required.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan image \u0026lt;image:tag\u0026gt; [flags]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eExactly one positional argument is required. There are no image-specific flags beyond the common scan flags, which include \u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e (repeatable, matched against file paths inside layers).\u003c/p\u003e\n\u003ch4 id=\"examples-2\"\u003eExamples\u003c/h4\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Scan a public image\nleakwatch scan image nginx:latest --format table\n\n# Scan a private registry image and save JSON output\nleakwatch scan image registry.example.com/my-app:v2.3.0 \\\n --format json \\\n --output image-results.json\n\u003c/code\u003e\u003c/pre\u003e\n\u003chr\u003e\n\u003ch3 id=\"scan-s3\"\u003e\u003ccode\u003escan s3\u003c/code\u003e\u003c/h3\u003e\n\u003cp\u003eScans objects in an AWS S3 bucket.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan s3 \u0026lt;bucket\u0026gt; [flags]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eExactly one positional argument is required.\u003c/p\u003e\n\u003ch4 id=\"s3-specific-flags\"\u003eS3-specific flags\u003c/h4\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--prefix \u0026lt;string\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eLimit the scan to objects whose key starts with this prefix.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--region \u0026lt;string\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eAWS region of the bucket. Falls back to \u003ccode\u003eAWS_REGION\u003c/code\u003e environment variable or the AWS SDK default.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eGlob pattern for object keys to exclude. Repeatable.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch4 id=\"examples-3\"\u003eExamples\u003c/h4\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Scan an entire bucket\nleakwatch scan s3 my-data-bucket --region us-east-1 --format table\n\n# Scan only a specific prefix\nleakwatch scan s3 my-data-bucket --prefix backups/2026/ --format json\n\u003c/code\u003e\u003c/pre\u003e\n\u003chr\u003e\n\u003ch3 id=\"scan-gcs\"\u003e\u003ccode\u003escan gcs\u003c/code\u003e\u003c/h3\u003e\n\u003cp\u003eScans objects in a Google Cloud Storage bucket.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan gcs \u0026lt;bucket\u0026gt; [flags]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eExactly one positional argument is required.\u003c/p\u003e\n\u003ch4 id=\"gcs-specific-flags\"\u003eGCS-specific flags\u003c/h4\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--prefix \u0026lt;string\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eLimit the scan to objects whose name starts with this prefix.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--project \u0026lt;string\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eGCP project ID. Required when the bucket's project cannot be inferred from the default credentials.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eGlob pattern for object names to exclude. Repeatable.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch4 id=\"examples-4\"\u003eExamples\u003c/h4\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Scan an entire GCS bucket\nleakwatch scan gcs my-gcs-bucket --project my-gcp-project --format table\n\n# Scan a prefix\nleakwatch scan gcs my-gcs-bucket --prefix uploads/2026/ --format json\n\u003c/code\u003e\u003c/pre\u003e\n\u003chr\u003e\n\u003ch3 id=\"scan-slack\"\u003e\u003ccode\u003escan slack\u003c/code\u003e\u003c/h3\u003e\n\u003cp\u003eScans message text in a Slack workspace.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan slack [flags]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eNo positional arguments.\u003c/p\u003e\n\u003ch4 id=\"slack-specific-flags\"\u003eSlack-specific flags\u003c/h4\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--token \u0026lt;string\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eSlack bot token. Can also be set via \u003ccode\u003eLEAKWATCH_SLACK_TOKEN\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--channels \u0026lt;list\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eComma-separated list of channel names or IDs to scan. Scans all accessible channels when omitted.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-channels \u0026lt;list\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eComma-separated list of channel names or IDs to skip.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--since \u0026lt;YYYY-MM-DD\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eScan only messages posted after this date.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--include-dms\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInclude direct messages (requires additional OAuth scopes).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--include-files\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDownload and scan bounded text-like attachments. Requires \u003ccode\u003efiles:read\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--rate-limit \u0026lt;float\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOptional per-operation request cap per second. Zero preserves Slack-safe operation-specific defaults.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003ccode\u003escan slack\u003c/code\u003e has no \u003ccode\u003e--exclude\u003c/code\u003e path-pattern flag (unlike every other scan subcommand) — use \u003ccode\u003e--exclude-channels\u003c/code\u003e to skip whole channels instead.\u003c/p\u003e\n\u003ch4 id=\"examples-5\"\u003eExamples\u003c/h4\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Scan all accessible channels\nleakwatch scan slack --token xoxb-••••••••••••-••••••••••••-•••••••••••••••••••••••• --format table\n\n# Scan specific channels since a date\nleakwatch scan slack \\\n --token xoxb-••••••••••••-••••••••••••-••••••••••••••••••••••••• \\\n --channels general,engineering \\\n --since 2026-01-01 \\\n --format json\n\u003c/code\u003e\u003c/pre\u003e\n\u003chr\u003e\n\u003ch3 id=\"scan-repos\"\u003e\u003ccode\u003escan repos\u003c/code\u003e\u003c/h3\u003e\n\u003cp\u003eScans multiple Git repositories in parallel.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan repos \u0026lt;url_or_path...\u0026gt; [flags]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eRequires at least two positional arguments (repository URLs or local paths).\u003c/p\u003e\n\u003ch4 id=\"repos-specific-flags\"\u003eRepos-specific flags\u003c/h4\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eShort\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--parallel\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNumber of repositories to scan concurrently.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-c\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCPU count\u003c/td\u003e\n\u003ctd\u003eWorker concurrency within each repository scan.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eGlob pattern for blob paths to exclude, applied to every repository. Repeatable.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch4 id=\"examples-6\"\u003eExamples\u003c/h4\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Scan two repositories in parallel\nleakwatch scan repos \\\n https://github.com/org/repo-a.git \\\n https://github.com/org/repo-b.git \\\n --format json\n\n# Increase parallelism for a large set of repos\nleakwatch scan repos \\\n https://github.com/org/repo-a.git \\\n https://github.com/org/repo-b.git \\\n https://github.com/org/repo-c.git \\\n --parallel 3 \\\n --format sarif \\\n --output multi-repo.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003chr\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/reference/exit-codes\"\u003eExit Codes\u003c/a\u003e — how exit codes map to scan outcomes.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/environment-variables\"\u003eEnvironment Variables\u003c/a\u003e — configure Leakwatch without flags.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/filesystem\"\u003eFilesystem Scanning\u003c/a\u003e — detailed \u003ccode\u003escan fs\u003c/code\u003e guide.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/git-history\"\u003eGit History\u003c/a\u003e — detailed \u003ccode\u003escan git\u003c/code\u003e guide.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eConfiguration File\u003c/a\u003e — \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e reference.\u003c/li\u003e\n\u003c/ul\u003e\n"},"reference/environment-variables":{"title":"Environment Variables","description":"Environment variables that configure Leakwatch behavior without flags.","html":"\u003ch1 id=\"environment-variables\"\u003eEnvironment Variables\u003c/h1\u003e\n\u003cp\u003eLeakwatch reads configuration from three sources in priority order: \u003cstrong\u003ecommand-line flags\u003c/strong\u003e override \u003cstrong\u003eenvironment variables\u003c/strong\u003e, which override the \u003cstrong\u003econfig file\u003c/strong\u003e (\u003ccode\u003e.leakwatch.yaml\u003c/code\u003e), which falls back to built-in \u003cstrong\u003edefaults\u003c/strong\u003e. Environment variables are useful in CI environments where you cannot modify a config file or pass flags to every invocation.\u003c/p\u003e\n\u003ch2 id=\"configuration-variable-pattern\"\u003eConfiguration variable pattern\u003c/h2\u003e\n\u003cp\u003eAny key from \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e can be set as an environment variable by:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eUppercasing the key name.\u003c/li\u003e\n\u003cli\u003eReplacing \u003ccode\u003e.\u003c/code\u003e and \u003ccode\u003e-\u003c/code\u003e with \u003ccode\u003e_\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003ePrepending \u003ccode\u003eLEAKWATCH_\u003c/code\u003e.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eFor example, the config key \u003ccode\u003escan.concurrency\u003c/code\u003e becomes \u003ccode\u003eLEAKWATCH_SCAN_CONCURRENCY\u003c/code\u003e.\u003c/p\u003e\n\u003ch2 id=\"variable-reference\"\u003eVariable reference\u003c/h2\u003e\n\u003ch3 id=\"leakwatch-specific-variables\"\u003eLeakwatch-specific variables\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eVariable\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_SLACK_TOKEN\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSlack bot token for \u003ccode\u003escan slack\u003c/code\u003e. Equivalent to \u003ccode\u003e--token\u003c/code\u003e. Set this instead of passing the token as a flag to avoid it appearing in shell history or CI logs.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_SCAN_CONCURRENCY\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNumber of concurrent scan workers. Equivalent to \u003ccode\u003e--concurrency\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_VERIFICATION_ENABLED\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSet to \u003ccode\u003efalse\u003c/code\u003e to disable live verification globally. Equivalent to \u003ccode\u003e--no-verify\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_VERIFICATION_RATE_LIMIT\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMaximum verification requests per second across all verifiers.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_OUTPUT_FORMAT\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDefault output format (\u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, or \u003ccode\u003egithub\u003c/code\u003e). Equivalent to \u003ccode\u003e--format\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_DETECTION_ENTROPY_THRESHOLD\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMinimum Shannon entropy for a match to be reported. Float value, e.g. \u003ccode\u003e3.5\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"display-variable\"\u003eDisplay variable\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eVariable\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eNO_COLOR\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eWhen set to any non-empty value, disables ANSI color codes in the \u003ccode\u003etable\u003c/code\u003e output formatter. Follows the \u003ca href=\"https://no-color.org\"\u003eno-color.org\u003c/a\u003e convention.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"aws-variables-for-scan-s3-and-aws-secret-verification\"\u003eAWS variables (for \u003ccode\u003escan s3\u003c/code\u003e and AWS secret verification)\u003c/h3\u003e\n\u003cp\u003eThese are standard AWS SDK environment variables. Leakwatch passes them through to the AWS SDK v2 default credential chain.\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eVariable\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eAWS_ACCESS_KEY_ID\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAWS access key ID.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eAWS_SECRET_ACCESS_KEY\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAWS secret access key.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eAWS_SESSION_TOKEN\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAWS session token (for temporary credentials).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eAWS_REGION\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDefault AWS region.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eAWS_PROFILE\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNamed profile from \u003ccode\u003e~/.aws/credentials\u003c/code\u003e to use.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"gcs-variable-for-scan-gcs\"\u003eGCS variable (for \u003ccode\u003escan gcs\u003c/code\u003e)\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eVariable\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eGOOGLE_APPLICATION_CREDENTIALS\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePath to a Google service-account JSON key file. Used by Application Default Credentials when scanning a GCS bucket.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"precedence-example\"\u003ePrecedence example\u003c/h2\u003e\n\u003cp\u003eGiven this setup:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e.leakwatch.yaml\u003c/code\u003e sets \u003ccode\u003eoutput.format: table\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eLEAKWATCH_OUTPUT_FORMAT=json\u003c/code\u003e is set in the environment\u003c/li\u003e\n\u003cli\u003eThe command is run as \u003ccode\u003eleakwatch scan fs .\u003c/code\u003e (no \u003ccode\u003e--format\u003c/code\u003e flag)\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe effective format is \u003ccode\u003ejson\u003c/code\u003e because the environment variable overrides the config file.\u003c/p\u003e\n\u003cp\u003eIf the command is run as \u003ccode\u003eleakwatch scan fs . --format sarif\u003c/code\u003e, the effective format is \u003ccode\u003esarif\u003c/code\u003e because the flag overrides everything.\u003c/p\u003e\n\u003ch2 id=\"credentials-for-verification-vs-credentials-for-scanning\"\u003eCredentials for verification vs. credentials for scanning\u003c/h2\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNote\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eThe AWS and GCP variables above are consumed to \u003cstrong\u003eauthenticate Leakwatch itself\u003c/strong\u003e when it connects to S3 or GCS to retrieve objects for scanning. They are not used to verify found secrets. Verification of a discovered AWS key, for example, uses that discovered key itself to call AWS STS — not the runner's credentials.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"passing-secrets-safely-in-ci\"\u003ePassing secrets safely in CI\u003c/h2\u003e\n\u003cp\u003eIn GitHub Actions, use encrypted secrets:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003eenv:\n LEAKWATCH_SLACK_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eIn GitLab CI, use masked CI/CD variables:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003evariables:\n LEAKWATCH_SLACK_TOKEN: $SLACK_BOT_TOKEN # defined as a masked variable in project settings\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eNever hard-code token values in workflow files or Dockerfiles.\u003c/p\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eConfiguration File\u003c/a\u003e — full \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e key reference.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/cloud-storage\"\u003eCloud Storage Scanning\u003c/a\u003e — \u003ccode\u003escan s3\u003c/code\u003e and \u003ccode\u003escan gcs\u003c/code\u003e credentials.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/slack\"\u003eSlack Scanning\u003c/a\u003e — Slack token scopes and permissions.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Reference\u003c/a\u003e — equivalent command-line flags.\u003c/li\u003e\n\u003c/ul\u003e\n"},"reference/exit-codes":{"title":"Exit Codes","description":"Leakwatch exit code reference and how to use them in scripts and CI pipelines.","html":"\u003ch1 id=\"exit-codes\"\u003eExit Codes\u003c/h1\u003e\n\u003cp\u003eLeakwatch uses a small, well-defined set of exit codes so that CI pipelines and shell scripts can act on scan results without parsing output. Every scan subcommand exits with one of four codes.\u003c/p\u003e\n\u003ch2 id=\"code-reference\"\u003eCode reference\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eCode\u003c/th\u003e\n\u003cth\u003eName\u003c/th\u003e\n\u003cth\u003eMeaning\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eClean\u003c/td\u003e\n\u003ctd\u003eThe scan completed successfully and no findings passed the active filters.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFindings\u003c/td\u003e\n\u003ctd\u003eThe scan completed and one or more secrets were found (and passed the active filters). This takes precedence over every other outcome: if findings were reported, exit code \u003ccode\u003e1\u003c/code\u003e is returned even if the scan was later interrupted or a repository in a multi-repo scan failed.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eError\u003c/td\u003e\n\u003ctd\u003eA hard error occurred — for example, an invalid flag, an unreadable path, or an authentication failure. An \u003ccode\u003eError: ...\u003c/code\u003e message and a usage hint are printed to stderr.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInterrupted\u003c/td\u003e\n\u003ctd\u003eThe scan received \u003ccode\u003eSIGINT\u003c/code\u003e (\u003ccode\u003eCtrl+C\u003c/code\u003e) or \u003ccode\u003eSIGTERM\u003c/code\u003e before it completed, and it had reported \u003cstrong\u003eno\u003c/strong\u003e findings at the point of interruption. Partial results (if any survived filtering) are still written to the configured output before the process exits. A CI pipeline can therefore never observe a clean exit \u003ccode\u003e0\u003c/code\u003e for a scan that did not run to completion.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"how-filters-affect-exit-code-1\"\u003eHow filters affect exit code 1\u003c/h2\u003e\n\u003cp\u003eExit code \u003ccode\u003e1\u003c/code\u003e is only emitted when at least one finding survives all active output filters. The two most relevant filters are:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/strong\u003e — findings below the threshold are suppressed. If all findings are \u003ccode\u003elow\u003c/code\u003e severity and you run with \u003ccode\u003e--min-severity high\u003c/code\u003e, exit code \u003ccode\u003e0\u003c/code\u003e is returned even though secrets exist.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/strong\u003e — only findings confirmed active by live verification are reported. If no active secrets are found, exit code \u003ccode\u003e0\u003c/code\u003e is returned.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThis means exit code \u003ccode\u003e0\u003c/code\u003e means \u0026quot;no findings matched your current filter settings\u0026quot; — not necessarily that the codebase contains no secrets at all.\u003c/p\u003e\n\u003cdiv class=\"callout callout-warn\"\u003e\u003cdiv class=\"callout-label\"\u003eWarning\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eA clean \u003ccode\u003e0\u003c/code\u003e exit under \u003ccode\u003e--only-verified\u003c/code\u003e does not guarantee the codebase is secret-free. Secrets for which verification is unavailable (11 detector types) are always reported as unverified and are suppressed by \u003ccode\u003e--only-verified\u003c/code\u003e. Pair \u003ccode\u003e--only-verified\u003c/code\u003e with a separate unfiltered scan if you need full coverage.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"using-exit-codes-in-shell-scripts\"\u003eUsing exit codes in shell scripts\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e#!/usr/bin/env bash\nset +e\nleakwatch scan fs . --format json --output leakwatch.json --no-verify\nEXIT_CODE=$?\nset -e\n\ncase \u0026quot;$EXIT_CODE\u0026quot; in\n 0)\n echo \u0026quot;No secrets found. Build continues.\u0026quot;\n ;;\n 1)\n echo \u0026quot;Secrets found — review leakwatch.json and remediate before merging.\u0026quot;\n exit 1\n ;;\n 3)\n echo \u0026quot;Scan was interrupted before it completed — treat as untrusted, do not merge.\u0026quot;\n exit 3\n ;;\n *)\n echo \u0026quot;Leakwatch encountered an error (exit $EXIT_CODE).\u0026quot;\n exit \u0026quot;$EXIT_CODE\u0026quot;\n ;;\nesac\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003eset +e\u003c/code\u003e before the scan prevents the shell from exiting on non-zero codes, giving you the chance to capture and handle the code yourself.\u003c/p\u003e\n\u003ch2 id=\"using-exit-codes-in-ci-pipelines\"\u003eUsing exit codes in CI pipelines\u003c/h2\u003e\n\u003cp\u003eMost CI systems treat any non-zero exit code as a step failure. Since Leakwatch exits \u003ccode\u003e1\u003c/code\u003e when secrets are found, the pipeline fails automatically without any extra configuration — simply run the scan command.\u003c/p\u003e\n\u003cp\u003eTo allow the pipeline to continue even when secrets are found (for example, to collect the report without blocking the build), explicitly ignore the exit code:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --format sarif --output results.sarif --no-verify || true\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eOr, in GitLab CI:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003eallow_failure: true\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eOr, in the GitHub Action, set \u003ccode\u003efail-on-findings: \u0026quot;false\u0026quot;\u003c/code\u003e.\u003c/p\u003e\n\u003ch2 id=\"exit-code-2-in-practice\"\u003eExit code 2 in practice\u003c/h2\u003e\n\u003cp\u003eExit code \u003ccode\u003e2\u003c/code\u003e indicates a configuration or runtime error that prevented the scan from running at all. Common causes:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eAn invalid flag value (for example, \u003ccode\u003e--format invalid\u003c/code\u003e).\u003c/li\u003e\n\u003cli\u003eA path that does not exist or is not readable.\u003c/li\u003e\n\u003cli\u003eA missing required argument (for example, \u003ccode\u003escan git\u003c/code\u003e with no URL).\u003c/li\u003e\n\u003cli\u003eAn authentication error when connecting to a cloud source.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe error message is printed to stderr and includes context to help diagnose the problem. For example, an invalid \u003ccode\u003e--format\u003c/code\u003e value produces:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eError: failed to load configuration: unsupported output format: xlsx\n\nRun 'leakwatch scan fs --help' for usage information.\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eValid \u003ccode\u003e--format\u003c/code\u003e values are \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, and \u003ccode\u003egithub\u003c/code\u003e.\u003c/p\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/other-ci\"\u003eOther CI Systems\u003c/a\u003e — how to wire exit codes into GitLab CI, Jenkins, and others.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/github-action\"\u003eGitHub Action\u003c/a\u003e — how the official action maps exit codes to step outcomes.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Reference\u003c/a\u003e — full flag reference.\u003c/li\u003e\n\u003c/ul\u003e\n"},"scanning/cloud-storage":{"title":"Cloud Storage (S3 \u0026 GCS)","description":"Scan AWS S3 and Google Cloud Storage buckets for leaked secrets.","html":"\u003ch1 id=\"cloud-storage-s3--gcs\"\u003eCloud Storage (S3 \u0026amp; GCS)\u003c/h1\u003e\n\u003cp\u003eSecrets regularly end up in cloud storage — exported database dumps, environment files, CI artefacts, and log archives all flow into buckets that may be readable by more people than intended. Leakwatch can scan AWS S3 and Google Cloud Storage buckets object-by-object and flag any secrets it finds before they become an incident.\u003c/p\u003e\n\u003ch2 id=\"aws-s3\"\u003eAWS S3\u003c/h2\u003e\n\u003ch3 id=\"usage\"\u003eUsage\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan s3 \u0026lt;bucket\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThe command takes exactly one argument: the \u003cstrong\u003ebucket name\u003c/strong\u003e (without the \u003ccode\u003es3://\u003c/code\u003e prefix). The scan target is displayed as \u003ccode\u003es3://\u0026lt;bucket\u0026gt;\u003c/code\u003e.\u003c/p\u003e\n\u003ch3 id=\"authentication\"\u003eAuthentication\u003c/h3\u003e\n\u003cp\u003eLeakwatch uses the standard \u003ca href=\"https://docs.aws.amazon.com/sdkref/latest/guide/standardized-credentials.html\"\u003eAWS default credential chain\u003c/a\u003e:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eEnvironment variables (\u003ccode\u003eAWS_ACCESS_KEY_ID\u003c/code\u003e, \u003ccode\u003eAWS_SECRET_ACCESS_KEY\u003c/code\u003e, \u003ccode\u003eAWS_SESSION_TOKEN\u003c/code\u003e).\u003c/li\u003e\n\u003cli\u003eShared credentials file (\u003ccode\u003e~/.aws/credentials\u003c/code\u003e).\u003c/li\u003e\n\u003cli\u003eShared configuration file (\u003ccode\u003e~/.aws/config\u003c/code\u003e).\u003c/li\u003e\n\u003cli\u003eIAM role attached to the instance or task (EC2, ECS, Lambda).\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eNo additional configuration is required if you are already authenticated with the AWS CLI (\u003ccode\u003eaws configure\u003c/code\u003e or an assumed role).\u003c/p\u003e\n\u003ch3 id=\"s3-specific-flags\"\u003eS3-specific flags\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eType\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--prefix\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eScan only objects whose key starts with this prefix.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--region\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003eFrom AWS config\u003c/td\u003e\n\u003ctd\u003eAWS region of the bucket.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"s3-examples\"\u003eS3 examples\u003c/h3\u003e\n\u003cp\u003eScan an entire bucket:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan s3 my-config-bucket\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eScan only objects under a specific key prefix in a given region:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan s3 my-bucket --prefix logs/ --region us-east-1\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eSave results as SARIF:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan s3 my-bucket --format sarif --output s3-results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eTip\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eUse \u003ccode\u003e--prefix\u003c/code\u003e to limit the scan to a relevant sub-path. Scanning a large bucket with millions of objects can be slow and may incur S3 GET request costs. Narrow the prefix to what actually matters — for example \u003ccode\u003econfigs/\u003c/code\u003e or \u003ccode\u003eexports/\u003c/code\u003e.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003chr\u003e\n\u003ch2 id=\"google-cloud-storage\"\u003eGoogle Cloud Storage\u003c/h2\u003e\n\u003ch3 id=\"usage-1\"\u003eUsage\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan gcs \u0026lt;bucket\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThe command takes exactly one argument: the \u003cstrong\u003ebucket name\u003c/strong\u003e (without the \u003ccode\u003egs://\u003c/code\u003e prefix). The scan target is displayed as \u003ccode\u003egs://\u0026lt;bucket\u0026gt;\u003c/code\u003e.\u003c/p\u003e\n\u003ch3 id=\"authentication-1\"\u003eAuthentication\u003c/h3\u003e\n\u003cp\u003eLeakwatch uses \u003ca href=\"https://cloud.google.com/docs/authentication/application-default-credentials\"\u003eApplication Default Credentials (ADC)\u003c/a\u003e. The credential search order is:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003ccode\u003eGOOGLE_APPLICATION_CREDENTIALS\u003c/code\u003e environment variable pointing to a service-account key file.\u003c/li\u003e\n\u003cli\u003eUser credentials set up by \u003ccode\u003egcloud auth application-default login\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003eService account attached to a Google Compute Engine instance, Cloud Run service, or GKE workload.\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"gcs-specific-flags\"\u003eGCS-specific flags\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eType\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--prefix\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eScan only objects whose name starts with this prefix.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--project\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eGCP project ID (required by some ADC configurations).\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"gcs-examples\"\u003eGCS examples\u003c/h3\u003e\n\u003cp\u003eScan an entire bucket with a specific GCP project:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan gcs my-config-bucket --project my-gcp-project\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eScan only objects under a specific prefix:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan gcs my-bucket --project my-gcp-project --prefix exports/\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eOutput as CSV:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan gcs my-bucket --format csv --output gcs-results.csv\n\u003c/code\u003e\u003c/pre\u003e\n\u003chr\u003e\n\u003ch2 id=\"common-scan-flags\"\u003eCommon scan flags\u003c/h2\u003e\n\u003cp\u003eBoth \u003ccode\u003es3\u003c/code\u003e and \u003ccode\u003egcs\u003c/code\u003e support the same common scan flags:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eShort\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-f\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ejson\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOutput format: \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, \u003ccode\u003egithub\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-o\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estdout\u003c/td\u003e\n\u003ctd\u003eWrite results to this file instead of stdout.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-c\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCPU count\u003c/td\u003e\n\u003ctd\u003eNumber of concurrent workers.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--max-file-size\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e10485760\u003c/code\u003e (10 MB)\u003c/td\u003e\n\u003ctd\u003eSkip objects larger than this value (bytes).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--show-raw\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInclude the raw secret value in output.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eObject-key pattern to exclude. Repeatable; combined with \u003ccode\u003efilter.exclude-paths\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-detectors\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eDetector IDs to exclude for this run. Repeatable; combined with \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--no-verify\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDisable secret verification.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eReport only findings confirmed active by verification.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMinimum severity to report: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e, \u003ccode\u003ecritical\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--remediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAttach remediation guidance to each finding.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003ePath-based exclusions (applied to object keys) are configured in \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e under \u003ccode\u003efilter.exclude-paths\u003c/code\u003e, or per-run via \u003ccode\u003e--exclude\u003c/code\u003e. Root-level flags \u003ccode\u003e--config\u003c/code\u003e and \u003ccode\u003e--log-level\u003c/code\u003e (default \u003ccode\u003ewarn\u003c/code\u003e) also apply.\u003c/p\u003e\n\u003ch2 id=\"finding-metadata\"\u003eFinding metadata\u003c/h2\u003e\n\u003cp\u003eAn S3 or GCS finding carries only the fields common to every source, plus \u003ccode\u003efile_path\u003c/code\u003e set to \u003ccode\u003e\u0026lt;bucket\u0026gt;/\u0026lt;object-key\u0026gt;\u003c/code\u003e (there is no separate \u003ccode\u003eline\u003c/code\u003e field for cloud-storage findings).\u003c/p\u003e\n\u003ch2 id=\"exit-codes\"\u003eExit codes\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eCode\u003c/th\u003e\n\u003cth\u003eMeaning\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan completed, no findings.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan completed, findings reported.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan failed (authentication error, bucket not found, etc.).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan was interrupted (\u003ccode\u003eCtrl+C\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e) before completing, and no findings had been reported.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eA scan summary is printed to stderr after every run. Scans cancel gracefully on SIGINT/SIGTERM.\u003c/p\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eQuick Start\u003c/a\u003e — run your first scan in under a minute.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eConfig File\u003c/a\u003e — configure exclusions and other defaults.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/ignoring-findings\"\u003eIgnoring Findings\u003c/a\u003e — suppress known false positives.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eHow Verification Works\u003c/a\u003e — understand verification statuses.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/filesystem\"\u003eFilesystem\u003c/a\u003e — scan a local directory tree.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Reference\u003c/a\u003e — full flag reference for all commands.\u003c/li\u003e\n\u003c/ul\u003e\n"},"scanning/container-images":{"title":"Container Images","description":"Scan OCI and Docker image layers for leaked secrets without a Docker daemon.","html":"\u003ch1 id=\"container-images\"\u003eContainer Images\u003c/h1\u003e\n\u003cp\u003eContainer images are a common hiding place for secrets: API keys baked into environment variables, credentials embedded in build layers, and configuration files copied into image layers and then forgotten. \u003ccode\u003eleakwatch scan image\u003c/code\u003e inspects every layer of an OCI or Docker image and surfaces those secrets before the image is deployed.\u003c/p\u003e\n\u003ch2 id=\"basic-usage\"\u003eBasic usage\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan image \u0026lt;image:tag\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThe command takes exactly one argument: an image reference in standard \u003ccode\u003ename:tag\u003c/code\u003e notation. Leakwatch uses \u003ca href=\"https://github.com/google/go-containerregistry\"\u003ego-containerregistry\u003c/a\u003e to pull and inspect images \u003cstrong\u003edaemonlessly\u003c/strong\u003e — no running Docker daemon is required.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Scan a Docker Hub image\nleakwatch scan image nginx:latest\n\n# Scan a private GitHub Container Registry image\nleakwatch scan image ghcr.io/org/myapp:v1.2.0\n\n# Scan an Amazon ECR image\nleakwatch scan image 123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"supported-registries\"\u003eSupported registries\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eRegistry\u003c/th\u003e\n\u003cth\u003eExample reference\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003eDocker Hub\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003enginx:latest\u003c/code\u003e, \u003ccode\u003emyorg/myapp:1.0.0\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eGitHub Container Registry (GHCR)\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eghcr.io/org/myapp:v1.2.0\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eAmazon ECR\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eGoogle Container Registry (GCR)\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003egcr.io/my-project/myapp:latest\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eAny OCI-compatible registry\u003c/td\u003e\n\u003ctd\u003eStandard \u003ccode\u003eregistry/name:tag\u003c/code\u003e form\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"authentication\"\u003eAuthentication\u003c/h2\u003e\n\u003cp\u003eLeakwatch uses the standard credential keychain used by Docker and other OCI tools. If you are already authenticated via \u003ccode\u003edocker login\u003c/code\u003e (or an equivalent tool such as \u003ccode\u003ecrane\u003c/code\u003e, \u003ccode\u003eskopeo\u003c/code\u003e, or cloud-provider credential helpers), Leakwatch will use those credentials automatically.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Log in to GHCR first\ndocker login ghcr.io\n\n# Then scan — credentials are picked up automatically\nleakwatch scan image ghcr.io/org/private-app:latest\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eFor Amazon ECR, configure the ECR credential helper or set \u003ccode\u003eAWS_ACCESS_KEY_ID\u003c/code\u003e and related environment variables before scanning.\u003c/p\u003e\n\u003ch2 id=\"how-it-scans\"\u003eHow it scans\u003c/h2\u003e\n\u003cp\u003eLeakwatch pulls the image manifest, iterates over each layer in order, and extracts the files within each layer. Each file's content is run through the same detection pipeline as a filesystem scan. Path exclusions from \u003ccode\u003efilter.exclude-paths\u003c/code\u003e in \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e (or \u003ccode\u003e--exclude\u003c/code\u003e) apply here, limiting which file paths inside layers are examined.\u003c/p\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNote\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eAs a defense against decompression-bomb layers, Leakwatch caps the cumulative decompressed bytes it will read: 2 GiB per layer and 10 GiB across the whole image. If a layer or image exceeds its ceiling, the affected layer's scan is truncated (a warning is logged) rather than let decompression run unbounded.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"flags\"\u003eFlags\u003c/h2\u003e\n\u003cp\u003eThere are no image-specific flags. All common scan flags apply:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eShort\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-f\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ejson\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOutput format: \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, \u003ccode\u003egithub\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-o\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estdout\u003c/td\u003e\n\u003ctd\u003eWrite results to this file instead of stdout.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-c\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCPU count\u003c/td\u003e\n\u003ctd\u003eNumber of concurrent workers.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--max-file-size\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e10485760\u003c/code\u003e (10 MB)\u003c/td\u003e\n\u003ctd\u003eSkip files larger than this value (bytes).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--show-raw\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInclude the raw secret value in output.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003ePath pattern to exclude. Repeatable; combined with \u003ccode\u003efilter.exclude-paths\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-detectors\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eDetector IDs to exclude for this run. Repeatable; combined with \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--no-verify\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDisable secret verification.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eReport only findings confirmed active by verification.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMinimum severity to report: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e, \u003ccode\u003ecritical\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--remediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAttach remediation guidance to each finding.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003ePath-based exclusions are configured in \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e under \u003ccode\u003efilter.exclude-paths\u003c/code\u003e, or per-run via \u003ccode\u003e--exclude\u003c/code\u003e. See \u003ca href=\"#/configuration/config-file\"\u003eConfig File\u003c/a\u003e for details.\u003c/p\u003e\n\u003cp\u003eRoot-level flags \u003ccode\u003e--config\u003c/code\u003e and \u003ccode\u003e--log-level\u003c/code\u003e (default \u003ccode\u003ewarn\u003c/code\u003e) also apply.\u003c/p\u003e\n\u003ch2 id=\"examples\"\u003eExamples\u003c/h2\u003e\n\u003cp\u003eScan a Docker Hub image and print results as a table:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan image alpine:3.20 --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eScan a private registry image and save SARIF output:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan image ghcr.io/org/myapp:v1.2.0 --format sarif -o results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eScan and show only verified active secrets:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan image myapp:latest --only-verified --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eInclude remediation guidance in JSON output:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan image myapp:latest --remediation --format json -o image-findings.json\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"finding-metadata\"\u003eFinding metadata\u003c/h2\u003e\n\u003cp\u003eEach finding from an image scan includes layer metadata:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eField\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eimage\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eThe image reference that was scanned.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003elayer\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eThe layer digest where the finding was detected (\u003ccode\u003e\u0026quot;config\u0026quot;\u003c/code\u003e for a finding in the image config, not a layer).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003elayer_idx\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eThe numeric index (0-based) of the layer within the image manifest; \u003ccode\u003e-1\u003c/code\u003e for a finding in the image config.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003efile_path\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eThe path of the file within the layer.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eTip\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eIntegrate container image scanning into your CI/CD pipeline's build stage to catch secrets before the image is pushed to a registry. Use \u003ccode\u003e--format sarif\u003c/code\u003e to upload results directly to GitHub Code Scanning.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"exit-codes\"\u003eExit codes\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eCode\u003c/th\u003e\n\u003cth\u003eMeaning\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan completed, no findings.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan completed, findings reported.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan failed (image not found, authentication error, etc.).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan was interrupted (\u003ccode\u003eCtrl+C\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e) before completing, and no findings had been reported.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eA scan summary is printed to stderr after every run. Scans cancel gracefully on SIGINT/SIGTERM.\u003c/p\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eQuick Start\u003c/a\u003e — run your first scan in under a minute.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/filesystem\"\u003eFilesystem\u003c/a\u003e — scan a local directory tree.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eConfig File\u003c/a\u003e — configure exclusions and other defaults.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/ignoring-findings\"\u003eIgnoring Findings\u003c/a\u003e — suppress known false positives.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eHow Verification Works\u003c/a\u003e — understand verification statuses.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Reference\u003c/a\u003e — full flag reference for all commands.\u003c/li\u003e\n\u003c/ul\u003e\n"},"scanning/filesystem":{"title":"Filesystem","description":"Scan a local directory tree for leaked secrets with leakwatch scan fs.","html":"\u003ch1 id=\"filesystem\"\u003eFilesystem\u003c/h1\u003e\n\u003cp\u003eLocal source code is where secrets most often appear first. The \u003ccode\u003eleakwatch scan fs\u003c/code\u003e command walks every file in a directory tree, runs the full detection pipeline on each one, and reports any findings before they can be committed — or after the fact on an existing codebase.\u003c/p\u003e\n\u003ch2 id=\"basic-usage\"\u003eBasic usage\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs [path...]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003epath\u003c/code\u003e accepts \u003cstrong\u003ezero or more\u003c/strong\u003e arguments; when omitted, Leakwatch scans the current working directory (\u003ccode\u003e.\u003c/code\u003e). Each path may be a directory (walked recursively) or a single file — you can mix files and directories, and pass several of each, in one invocation.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Scan the current directory\nleakwatch scan fs\n\n# Scan a specific project folder\nleakwatch scan fs ./my-project\n\n# Scan a single file\nleakwatch scan fs cmd/main.go\n\n# Scan several files and directories together\nleakwatch scan fs cmd/ main.go internal/config\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"what-the-filesystem-source-skips-automatically\"\u003eWhat the filesystem source skips automatically\u003c/h2\u003e\n\u003cp\u003eTo keep scans fast and noise-free, the filesystem source skips the following without any configuration:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eBinary files\u003c/strong\u003e — detected by the presence of a null byte in the first 8 KB of the file.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eKnown binary extensions\u003c/strong\u003e — common compiled, image, audio, video, and archive formats.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLock files\u003c/strong\u003e — \u003ccode\u003epackage-lock.json\u003c/code\u003e, \u003ccode\u003eyarn.lock\u003c/code\u003e, \u003ccode\u003ePipfile.lock\u003c/code\u003e, and similar.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003e.git\u003c/code\u003e directories\u003c/strong\u003e — the Git object/pack store is never walked or read as plain files, since \u003ccode\u003escan git\u003c/code\u003e is the dedicated way to scan commit history. This applies to any \u003ccode\u003e.git\u003c/code\u003e directory encountered while walking; if you explicitly pass \u003ccode\u003e.git\u003c/code\u003e itself as the scan path, that exemption does not apply and it is scanned normally. Also unaffected: symlinks are always skipped to avoid cycles.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"flags\"\u003eFlags\u003c/h2\u003e\n\u003ch3 id=\"filesystem-specific\"\u003eFilesystem-specific\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eType\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring (repeatable)\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eGlob patterns for paths to exclude. Can be repeated or comma-separated.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"common-scan-flags\"\u003eCommon scan flags\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eShort\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-f\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ejson\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOutput format: \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, \u003ccode\u003egithub\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-o\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estdout\u003c/td\u003e\n\u003ctd\u003eWrite results to this file instead of stdout.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-c\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCPU count\u003c/td\u003e\n\u003ctd\u003eNumber of concurrent workers.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--max-file-size\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e10485760\u003c/code\u003e (10 MB)\u003c/td\u003e\n\u003ctd\u003eSkip files larger than this value (bytes).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--show-raw\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInclude the raw secret value in output.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-detectors\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eDetector IDs to exclude for this run. Repeatable; combined with \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--no-verify\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDisable secret verification.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eReport only findings confirmed active by verification.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMinimum severity to report: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e, \u003ccode\u003ecritical\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--remediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAttach remediation guidance to each finding.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eRoot-level flags \u003ccode\u003e--config\u003c/code\u003e and \u003ccode\u003e--log-level\u003c/code\u003e (default \u003ccode\u003ewarn\u003c/code\u003e) also apply.\u003c/p\u003e\n\u003ch2 id=\"examples\"\u003eExamples\u003c/h2\u003e\n\u003cp\u003eScan the current directory and print a colorized table to the terminal:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eExclude test files and vendor directories, then save SARIF output for GitHub Code Scanning:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . \\\n --exclude \u0026quot;**/*_test.go\u0026quot; \\\n --exclude \u0026quot;vendor/**\u0026quot; \\\n --format sarif \\\n --output results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eLimit file size to 5 MB and increase worker count for a large monorepo:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --max-file-size 5242880 --concurrency 8 --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eShow only high-severity findings and include rotation instructions:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --min-severity high --remediation --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"excluding-paths\"\u003eExcluding paths\u003c/h2\u003e\n\u003cp\u003eThe \u003ccode\u003e--exclude\u003c/code\u003e flag accepts glob patterns and can be specified multiple times or as a comma-separated list:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Two separate flags\nleakwatch scan fs . --exclude \u0026quot;**/*_test.go\u0026quot; --exclude \u0026quot;docs/**\u0026quot;\n\n# Comma-separated\nleakwatch scan fs . --exclude \u0026quot;**/*_test.go,docs/**\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eFor permanent exclusion rules shared across your team, add them to \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e under \u003ccode\u003efilter.exclude-paths\u003c/code\u003e. Those rules apply to every source, not just filesystem scans. You can also create a \u003ccode\u003e.leakwatchignore\u003c/code\u003e file in your project root. See \u003ca href=\"#/configuration/config-file\"\u003eConfig File\u003c/a\u003e and \u003ca href=\"#/configuration/ignoring-findings\"\u003eIgnoring Findings\u003c/a\u003e for details.\u003c/p\u003e\n\u003ch2 id=\"exit-codes\"\u003eExit codes\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eCode\u003c/th\u003e\n\u003cth\u003eMeaning\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan completed, no findings.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan completed, findings reported.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan failed (configuration error, unreadable path, etc.).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan was interrupted (\u003ccode\u003eCtrl+C\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e) before completing, and no findings had been reported.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eA scan summary (source type, target, file count, duration, and finding count) is printed to stderr after every run. Scans cancel gracefully on SIGINT/SIGTERM.\u003c/p\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eTip\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eRun \u003ccode\u003eleakwatch scan fs . --format table\u003c/code\u003e during development to get a quick visual overview. Switch to \u003ccode\u003e--format sarif\u003c/code\u003e in CI pipelines to integrate with GitHub Code Scanning.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eQuick Start\u003c/a\u003e — run your first scan in under a minute.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eConfig File\u003c/a\u003e — configure default format, exclusions, and more.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/ignoring-findings\"\u003eIgnoring Findings\u003c/a\u003e — \u003ccode\u003e.leakwatchignore\u003c/code\u003e and inline suppression.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eHow Verification Works\u003c/a\u003e — understand verification statuses.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/git-history\"\u003eGit History\u003c/a\u003e — scan committed history, not just the working tree.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Reference\u003c/a\u003e — full flag reference for all commands.\u003c/li\u003e\n\u003c/ul\u003e\n"},"scanning/git-history":{"title":"Git History","description":"Scan the full commit history of a local or remote Git repository for leaked secrets.","html":"\u003ch1 id=\"git-history\"\u003eGit History\u003c/h1\u003e\n\u003cp\u003eA secret that was committed and then deleted is still present in every earlier commit, reachable to anyone with repository access. \u003ccode\u003eleakwatch scan git\u003c/code\u003e walks the \u003cem\u003eentire\u003c/em\u003e commit history of a repository — local or remote — and surfaces those secrets before they can be exploited.\u003c/p\u003e\n\u003ch2 id=\"basic-usage\"\u003eBasic usage\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git \u0026lt;url_or_path\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThe command takes exactly one argument: either a \u003cstrong\u003elocal filesystem path\u003c/strong\u003e to a repository (\u003ccode\u003e.\u003c/code\u003e for the current directory) or a \u003cstrong\u003eremote HTTP/HTTPS or SSH URL\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eLeakwatch uses \u003ca href=\"https://github.com/go-git/go-git\"\u003ego-git\u003c/a\u003e for all Git operations — a pure Go implementation with no dependency on a system \u003ccode\u003egit\u003c/code\u003e binary.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Scan the local repository in the current directory\nleakwatch scan git .\n\n# Scan a remote repository over HTTPS\nleakwatch scan git https://github.com/org/repo.git\n\n# Scan over SSH\nleakwatch scan git git@github.com:org/repo.git\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"how-it-scans\"\u003eHow it scans\u003c/h2\u003e\n\u003cp\u003eLeakwatch walks every commit in the history and examines the blobs introduced by each commit. \u003cstrong\u003eBlob-hash deduplication\u003c/strong\u003e ensures that identical file content is scanned only once, no matter how many commits reference it. This keeps scan time proportional to the \u003cem\u003eunique content\u003c/em\u003e in the repository rather than to the raw commit count.\u003c/p\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNote\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eBecause Leakwatch examines commit-by-commit diffs, it finds secrets that were introduced and later deleted — content that is invisible in the current working tree.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"flags\"\u003eFlags\u003c/h2\u003e\n\u003ch3 id=\"git-specific\"\u003eGit-specific\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eType\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--since\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring (YYYY-MM-DD)\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eScan only commits after this date.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--since-commit\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eScan only changes from this commit hash to HEAD (diff-based).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--branch\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eTarget a specific branch instead of the default.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--depth\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eint\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e (full)\u003c/td\u003e\n\u003ctd\u003eClone depth for \u003cstrong\u003eremote repositories only\u003c/strong\u003e. \u003ccode\u003e0\u003c/code\u003e means full history.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"common-scan-flags\"\u003eCommon scan flags\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eShort\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-f\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ejson\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOutput format: \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, \u003ccode\u003egithub\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-o\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estdout\u003c/td\u003e\n\u003ctd\u003eWrite results to this file instead of stdout.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-c\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCPU count\u003c/td\u003e\n\u003ctd\u003eNumber of concurrent workers.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--max-file-size\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e10485760\u003c/code\u003e (10 MB)\u003c/td\u003e\n\u003ctd\u003eSkip blobs larger than this value (bytes).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--show-raw\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInclude the raw secret value in output.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003ePath pattern to exclude. Repeatable; combined with \u003ccode\u003efilter.exclude-paths\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-detectors\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eDetector IDs to exclude for this run. Repeatable; combined with \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--no-verify\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDisable secret verification.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eReport only findings confirmed active by verification.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMinimum severity to report: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e, \u003ccode\u003ecritical\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--remediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAttach remediation guidance to each finding.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eRoot-level flags \u003ccode\u003e--config\u003c/code\u003e and \u003ccode\u003e--log-level\u003c/code\u003e (default \u003ccode\u003ewarn\u003c/code\u003e) also apply.\u003c/p\u003e\n\u003ch2 id=\"examples\"\u003eExamples\u003c/h2\u003e\n\u003cp\u003eScan the full history of the local repository and print a table:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git . --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eScan only commits made after a specific date on the \u003ccode\u003edevelop\u003c/code\u003e branch:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git . --since 2026-02-23 --branch develop\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eScan changes introduced since a specific commit (useful in CI to check only new commits):\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git . --since-commit a1b2c3d\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eDo a shallow clone of a large remote repository to speed up the initial scan:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git https://github.com/org/repo.git --depth 50\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eScan a remote repository and save verified findings only as SARIF:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git https://github.com/org/repo.git \\\n --only-verified \\\n --format sarif \\\n --output git-results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"finding-metadata\"\u003eFinding metadata\u003c/h2\u003e\n\u003cp\u003eEach finding from a Git scan includes commit metadata:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eField\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003erepository\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eURL or path of the scanned repository (credentials stripped).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecommit\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCommit hash where the secret was introduced.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eauthor\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCommit author's name only.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eemail\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCommit author's email address, as a separate field.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edate\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCommit timestamp.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ebranch\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBranch context (when available).\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eTip\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eUse \u003ccode\u003e--since-commit\u003c/code\u003e in pull-request CI jobs to scan only the commits added by the PR. Use \u003ccode\u003e--since \u0026lt;date\u0026gt;\u003c/code\u003e for scheduled nightly scans covering recent activity.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"credential-safety\"\u003eCredential safety\u003c/h2\u003e\n\u003cp\u003eWhen a repository URL contains embedded credentials (for example \u003ccode\u003ehttps://user:TOKEN@host/repo.git\u003c/code\u003e), Leakwatch strips those credentials before writing anything to logs or output, so the token never appears in scan results or CI traces.\u003c/p\u003e\n\u003ch2 id=\"exit-codes\"\u003eExit codes\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eCode\u003c/th\u003e\n\u003cth\u003eMeaning\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan completed, no findings.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan completed, findings reported.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan failed (invalid URL, authentication error, etc.).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan was interrupted (\u003ccode\u003eCtrl+C\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e) before completing, and no findings had been reported.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eA scan summary is printed to stderr after every run. Scans cancel gracefully on SIGINT/SIGTERM.\u003c/p\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eQuick Start\u003c/a\u003e — run your first scan in under a minute.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/multiple-repos\"\u003eMultiple Repositories\u003c/a\u003e — scan several repositories in one command.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/filesystem\"\u003eFilesystem\u003c/a\u003e — scan the working tree instead of history.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eHow Verification Works\u003c/a\u003e — understand verification statuses.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/ignoring-findings\"\u003eIgnoring Findings\u003c/a\u003e — suppress known false positives.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Reference\u003c/a\u003e — full flag reference for all commands.\u003c/li\u003e\n\u003c/ul\u003e\n"},"scanning/multiple-repos":{"title":"Multiple Repositories","description":"Scan several Git repositories concurrently and combine results into a single report.","html":"\u003ch1 id=\"multiple-repositories\"\u003eMultiple Repositories\u003c/h1\u003e\n\u003cp\u003eWhen an organization grows, secrets can land in any of dozens or hundreds of repositories. Checking them one by one is impractical. \u003ccode\u003eleakwatch scan repos\u003c/code\u003e accepts multiple repository URLs and scans them concurrently, merging all findings into a single output — one command, one report.\u003c/p\u003e\n\u003ch2 id=\"basic-usage\"\u003eBasic usage\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan repos \u0026lt;url1\u0026gt; \u0026lt;url2\u0026gt; [url...]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThe command requires \u003cstrong\u003eat least two\u003c/strong\u003e repository URLs. All repositories are cloned, scanned, and cleaned up automatically. The combined finding count and a single scan summary are reported at the end.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan repos \\\n https://github.com/org/api.git \\\n https://github.com/org/web.git\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"how-it-works\"\u003eHow it works\u003c/h2\u003e\n\u003cp\u003eLeakwatch spawns up to \u003ccode\u003e--parallel\u003c/code\u003e repository scans at once. Each repository is:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eCloned from the provided URL (credentials are stripped from logs and output for safety).\u003c/li\u003e\n\u003cli\u003eScanned with the full detection pipeline, using \u003ccode\u003e--concurrency\u003c/code\u003e workers for that repository.\u003c/li\u003e\n\u003cli\u003eCleaned up (the temporary clone is deleted) once the scan completes.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eAll findings from all repositories are collected and written as a single output, as if the scan had been a single-source run. The displayed target is \u003ccode\u003e\u0026lt;N\u0026gt; repositories\u003c/code\u003e.\u003c/p\u003e\n\u003ch2 id=\"flags\"\u003eFlags\u003c/h2\u003e\n\u003ch3 id=\"multi-repo-specific\"\u003eMulti-repo-specific\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eType\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--parallel\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eint\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNumber of repositories to scan in parallel.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"common-scan-flags\"\u003eCommon scan flags\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eShort\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-f\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ejson\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOutput format: \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, \u003ccode\u003egithub\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-o\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estdout\u003c/td\u003e\n\u003ctd\u003eWrite results to this file instead of stdout.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-c\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCPU count\u003c/td\u003e\n\u003ctd\u003eNumber of concurrent workers \u003cstrong\u003eper repository\u003c/strong\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--max-file-size\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e10485760\u003c/code\u003e (10 MB)\u003c/td\u003e\n\u003ctd\u003eSkip blobs larger than this value (bytes).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--show-raw\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInclude the raw secret value in output.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003ePath pattern to exclude from every repository's scan. Repeatable; combined with \u003ccode\u003efilter.exclude-paths\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-detectors\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eDetector IDs to exclude for this run (e.g. \u003ccode\u003eaws-access-key-id\u003c/code\u003e). Repeatable; combined with \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--no-verify\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDisable secret verification.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eReport only findings confirmed active by verification.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMinimum severity to report: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e, \u003ccode\u003ecritical\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--remediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAttach remediation guidance to each finding.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003ePath exclusions from \u003ccode\u003efilter.exclude-paths\u003c/code\u003e in \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e (or \u003ccode\u003e--exclude\u003c/code\u003e) apply to all repositories. Root-level flags \u003ccode\u003e--config\u003c/code\u003e and \u003ccode\u003e--log-level\u003c/code\u003e (default \u003ccode\u003ewarn\u003c/code\u003e) also apply.\u003c/p\u003e\n\u003ch2 id=\"examples\"\u003eExamples\u003c/h2\u003e\n\u003cp\u003eScan two repositories and display results as a table:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan repos \\\n https://github.com/org/api.git \\\n https://github.com/org/web.git \\\n --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eScan five repositories with higher parallelism and save the combined results as SARIF:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan repos \\\n https://github.com/org/api.git \\\n https://github.com/org/web.git \\\n https://github.com/org/infra.git \\\n https://github.com/org/mobile.git \\\n https://github.com/org/docs.git \\\n --parallel 4 \\\n --format sarif \\\n --output all-repos.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eScan with more workers per repository and show only verified findings:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan repos \\\n https://github.com/org/backend.git \\\n https://github.com/org/frontend.git \\\n --concurrency 8 \\\n --only-verified \\\n --format json \\\n --output verified-findings.json\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"tuning-parallelism\"\u003eTuning parallelism\u003c/h2\u003e\n\u003cp\u003eTwo knobs control throughput:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e--parallel\u003c/code\u003e controls how many repository clones and scans run simultaneously. The default of \u003ccode\u003e3\u003c/code\u003e is appropriate for most workloads. Raise it when network bandwidth and CPU headroom allow; lower it on constrained machines.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e--concurrency\u003c/code\u003e (\u003ccode\u003e-c\u003c/code\u003e) controls how many worker goroutines process file blobs \u003cem\u003ewithin\u003c/em\u003e each individual repository. This is the same flag available on all scan commands.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eTotal concurrent operations at peak = \u003ccode\u003e--parallel\u003c/code\u003e × \u003ccode\u003e--concurrency\u003c/code\u003e.\u003c/p\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNote\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eIf one or more repository scans fail (for example, due to a network error or authentication failure), Leakwatch logs the error and continues scanning the remaining repositories. \u003cstrong\u003eFindings take precedence\u003c/strong\u003e: if any finding survives filtering across the whole run, the exit code is \u003ccode\u003e1\u003c/code\u003e even though some repositories failed to scan. Exit code \u003ccode\u003e2\u003c/code\u003e (repository scan failure) is only returned when zero findings were reported overall.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"credential-safety\"\u003eCredential safety\u003c/h2\u003e\n\u003cp\u003eAny embedded credentials in repository URLs (e.g. \u003ccode\u003ehttps://user:TOKEN@host/repo.git\u003c/code\u003e) are stripped before the URL is written to logs, output, or the scan summary.\u003c/p\u003e\n\u003ch2 id=\"exit-codes\"\u003eExit codes\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eCode\u003c/th\u003e\n\u003cth\u003eMeaning\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAll repository scans completed, no findings.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOne or more findings survived filtering — returned even if some repositories failed to scan or the run was interrupted, so a \u0026quot;secrets found\u0026quot; signal is never masked by an unrelated failure.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eZero findings were reported and one or more repository scans failed, or a configuration error occurred.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eThe run was interrupted (\u003ccode\u003eCtrl+C\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e) before completing and zero findings had been reported.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eA scan summary is printed to stderr after every run. Scans cancel gracefully on SIGINT/SIGTERM.\u003c/p\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/git-history\"\u003eGit History\u003c/a\u003e — scan a single repository in depth.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eQuick Start\u003c/a\u003e — run your first scan in under a minute.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eConfig File\u003c/a\u003e — configure shared defaults for all sources.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/ignoring-findings\"\u003eIgnoring Findings\u003c/a\u003e — suppress known false positives.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eHow Verification Works\u003c/a\u003e — understand verification statuses.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Reference\u003c/a\u003e — full flag reference for all commands.\u003c/li\u003e\n\u003c/ul\u003e\n"},"scanning/slack":{"title":"Slack Workspace","description":"Scan Slack messages and opt-in text attachments for leaked secrets.","html":"\u003ch1 id=\"slack-workspace\"\u003eSlack Workspace\u003c/h1\u003e\n\u003cp\u003eDevelopers frequently share credentials in chat — a token pasted into a channel for a quick test, a password sent in a DM, or a configuration file uploaded to an incident thread. \u003ccode\u003eleakwatch scan slack\u003c/code\u003e reads message text across your Slack workspace and, when explicitly enabled, text-like file attachments.\u003c/p\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNote\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eFile attachment scanning is opt-in. Use \u003ccode\u003e--include-files\u003c/code\u003e and grant \u003ccode\u003efiles:read\u003c/code\u003e; without the flag, Leakwatch downloads no attachments.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"basic-usage\"\u003eBasic usage\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan slack\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThis command takes \u003cstrong\u003eno positional arguments\u003c/strong\u003e. All configuration is provided through flags or environment variables.\u003c/p\u003e\n\u003ch2 id=\"authentication\"\u003eAuthentication\u003c/h2\u003e\n\u003cp\u003eA Slack Bot Token is required. Provide it via the \u003ccode\u003e--token\u003c/code\u003e flag or the \u003ccode\u003eLEAKWATCH_SLACK_TOKEN\u003c/code\u003e environment variable. Using an environment variable is recommended so the token never appears in shell history or process listings.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eexport LEAKWATCH_SLACK_TOKEN=xoxb-...\nleakwatch scan slack\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"required-bot-token-scopes\"\u003eRequired bot token scopes\u003c/h3\u003e\n\u003cp\u003eThe bot token must be associated with a Slack app that has the following OAuth scopes:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eScope\u003c/th\u003e\n\u003cth\u003ePurpose\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003echannels:read\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eList public channels.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003echannels:history\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRead messages in public channels the bot has joined.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egroups:read\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eList private channels.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egroups:history\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRead messages in private channels the bot has joined.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eim:read\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eList direct-message conversations (required only with \u003ccode\u003e--include-dms\u003c/code\u003e).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eim:history\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRead direct messages (required only with \u003ccode\u003e--include-dms\u003c/code\u003e).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003empim:read\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eList group direct-message conversations (required only with \u003ccode\u003e--include-dms\u003c/code\u003e).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003empim:history\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRead group direct messages (required only with \u003ccode\u003e--include-dms\u003c/code\u003e).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003efiles:read\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRead file metadata and contents (required only with \u003ccode\u003e--include-files\u003c/code\u003e).\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"flags\"\u003eFlags\u003c/h2\u003e\n\u003ch3 id=\"slack-specific\"\u003eSlack-specific\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eType\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eSlack Bot Token. Prefer \u003ccode\u003eLEAKWATCH_SLACK_TOKEN\u003c/code\u003e env var.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--channels\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003eall channels\u003c/td\u003e\n\u003ctd\u003eComma-separated list of channel names to scan.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-channels\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eComma-separated list of channel names to skip.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--since\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring (YYYY-MM-DD)\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eScan messages posted on or after this date.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--include-dms\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ebool\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAlso scan direct messages and group DMs.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--include-files\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ebool\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDownload and scan bounded text-like file attachments. Requires \u003ccode\u003efiles:read\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--rate-limit\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003efloat\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOptional per-operation Slack request cap per second. Zero retains safe operation-specific defaults: history uses \u003ccode\u003e1/60\u003c/code\u003e, while list/file/download defaults are higher.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"common-scan-flags\"\u003eCommon scan flags\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eFlag\u003c/th\u003e\n\u003cth\u003eShort\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-f\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ejson\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOutput format: \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, \u003ccode\u003egithub\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-o\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estdout\u003c/td\u003e\n\u003ctd\u003eWrite results to this file instead of stdout.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-c\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCPU count\u003c/td\u003e\n\u003ctd\u003eNumber of concurrent workers.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--max-file-size\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e10485760\u003c/code\u003e (10 MB)\u003c/td\u003e\n\u003ctd\u003eMaximum attachment size buffered for scanning, in bytes.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--show-raw\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInclude the raw secret value in output.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-detectors\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eDetector IDs to exclude for this run. Repeatable; combined with \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--no-verify\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDisable secret verification.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eReport only findings confirmed active by verification.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMinimum severity to report: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e, \u003ccode\u003ecritical\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--remediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAttach remediation guidance to each finding.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eRoot-level flags \u003ccode\u003e--config\u003c/code\u003e and \u003ccode\u003e--log-level\u003c/code\u003e (default \u003ccode\u003ewarn\u003c/code\u003e) also apply. Unlike every other scan subcommand, \u003ccode\u003escan slack\u003c/code\u003e has no \u003ccode\u003e--exclude\u003c/code\u003e path-pattern flag — use \u003ccode\u003e--exclude-channels\u003c/code\u003e to skip whole channels instead.\u003c/p\u003e\n\u003ch2 id=\"examples\"\u003eExamples\u003c/h2\u003e\n\u003cp\u003eScan all channels the bot has access to, using an environment variable for the token:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eexport LEAKWATCH_SLACK_TOKEN=xoxb-...\nleakwatch scan slack\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eScan specific channels and limit to messages since the start of the year:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan slack \\\n --channels general,engineering,backend \\\n --since 2026-01-01\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eExclude noisy channels and include direct messages:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan slack \\\n --exclude-channels random,social,giphy \\\n --include-dms\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eInclude text-like file attachments while lowering the per-file memory bound:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan slack \\\n --include-files \\\n --max-file-size 5242880\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eRaise the API request rate only for a Marketplace/internal app whose published tier permits it:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan slack --rate-limit 0.8 --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eSave only verified active findings to a JSON file:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan slack \\\n --only-verified \\\n --format json \\\n --output slack-findings.json\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"finding-metadata\"\u003eFinding metadata\u003c/h2\u003e\n\u003cp\u003eEach finding from a Slack scan includes message and channel metadata:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eField\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003echannel\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eThe Slack channel \u003cstrong\u003eID\u003c/strong\u003e where the finding was detected (e.g. \u003ccode\u003eC0123456\u003c/code\u003e) — not the human-readable name.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003echannel_name\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eThe human-readable channel name (e.g. \u003ccode\u003eengineering\u003c/code\u003e), as a separate field from \u003ccode\u003echannel\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003emessage_user\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSlack user ID of the message author. There is no \u003ccode\u003eauthor\u003c/code\u003e field for Slack findings.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003emessage_ts\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSlack message timestamp (unique message ID).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ethread_ts\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTimestamp of the parent message, present only when the finding is in a threaded reply.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003efile_path\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSynthetic \u003ccode\u003eslack/\u0026lt;channel\u0026gt;/\u0026lt;filename\u0026gt;\u003c/code\u003e path, present for findings from an attachment.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"performance-considerations\"\u003ePerformance considerations\u003c/h2\u003e\n\u003cp\u003eSlack API limits apply per method, workspace, and app. Leakwatch therefore uses independent limiter buckets: \u003ccode\u003econversations.history\u003c/code\u003e defaults to one request/minute for newly distributed non-Marketplace apps, \u003ccode\u003econversations.list\u003c/code\u003e follows Tier 2 (20+/minute), \u003ccode\u003efiles.info\u003c/code\u003e follows Tier 4 (100+/minute), and attachment downloads have a separate conservative 100/minute client-side cap. \u003ccode\u003e--rate-limit\u003c/code\u003e deliberately overrides every bucket with the same per-operation ceiling; Marketplace/internal apps with Tier 3 history access can raise it deliberately.\u003c/p\u003e\n\u003cp\u003eWhen Slack responds with \u003ccode\u003e429 Too Many Requests\u003c/code\u003e, Leakwatch automatically honors the \u003ccode\u003eRetry-After\u003c/code\u003e header and retries the request rather than failing the scan outright.\u003c/p\u003e\n\u003cp\u003eAttachment metadata and downloads use separate limiter buckets rather than consuming each other's or history capacity. Leakwatch accepts only Slack-owned HTTPS download URLs, transfers attachment chunks with direct backpressure, buffers at most \u003ccode\u003e--max-file-size\u003c/code\u003e, rejects invalid UTF-8/binary content even when MIME metadata is empty or spoofed, and de-duplicates the same Slack file ID across messages.\u003c/p\u003e\n\u003cp\u003eUse \u003ccode\u003e--channels\u003c/code\u003e to target specific channels rather than scanning the entire workspace on every run. Combine with \u003ccode\u003e--since\u003c/code\u003e to scan only recent messages incrementally.\u003c/p\u003e\n\u003ch2 id=\"exit-codes\"\u003eExit codes\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eCode\u003c/th\u003e\n\u003cth\u003eMeaning\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan completed, no findings.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan completed, findings reported.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan failed (missing token, authentication error, etc.).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eScan was interrupted (\u003ccode\u003eCtrl+C\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e) before completing, and no findings had been reported.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eA scan summary is printed to stderr after every run. Scans cancel gracefully on SIGINT/SIGTERM.\u003c/p\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eQuick Start\u003c/a\u003e — run your first scan in under a minute.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eConfig File\u003c/a\u003e — configure defaults in \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/ignoring-findings\"\u003eIgnoring Findings\u003c/a\u003e — suppress known false positives.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eHow Verification Works\u003c/a\u003e — understand verification statuses.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/git-history\"\u003eGit History\u003c/a\u003e — scan committed history for secrets.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Reference\u003c/a\u003e — full flag reference for all commands.\u003c/li\u003e\n\u003c/ul\u003e\n"},"verification/how-verification-works":{"title":"How Verification Works","description":"How Leakwatch confirms whether a detected secret is still active, which verification modes it uses, and how to configure or disable verification.","html":"\u003ch1 id=\"how-verification-works\"\u003eHow Verification Works\u003c/h1\u003e\n\u003cp\u003eFinding a secret in a codebase is only half the story. A key that was rotated six months ago is noise; a key that is still live is an active incident. Verification is the step that draws that line — it takes each detected finding and, where possible, confirms whether the secret is currently valid at the provider.\u003c/p\u003e\n\u003ch2 id=\"from-detection-to-verification\"\u003eFrom detection to verification\u003c/h2\u003e\n\u003cp\u003eAfter the scan engine collects findings, the verifier pool picks them up. Each finding carries a \u003ccode\u003edetector_id\u003c/code\u003e; Leakwatch looks up whether a verifier is registered for that ID:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eIf a verifier exists, it runs and returns a status.\u003c/li\u003e\n\u003cli\u003eIf no verifier is registered for that detector type, the finding passes through unchanged with status \u003ccode\u003eunverified\u003c/code\u003e.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"three-verification-modes\"\u003eThree verification modes\u003c/h2\u003e\n\u003cp\u003eNot all secrets can be verified the same way. Leakwatch distinguishes direct live checks, checks that require trusted or companion context, and offline format validation. Registry presence is therefore not treated as proof of live capability.\u003c/p\u003e\n\u003ch3 id=\"live-api-verification\"\u003eLive API verification\u003c/h3\u003e\n\u003cp\u003eFor 39 detector types, Leakwatch can make a \u003cstrong\u003econtrolled, non-destructive provider check\u003c/strong\u003e in the normal production path — for example, calling \u003ccode\u003ests:GetCallerIdentity\u003c/code\u003e for AWS keys or a fixed provider identity endpoint for OpenAI keys. The call uses only the minimum endpoint required to confirm identity; it never modifies data or creates resources, though it may consume provider quota.\u003c/p\u003e\n\u003cp\u003eIf the provider returns a contract-valid success response, the finding is marked \u003ccode\u003everified_active\u003c/code\u003e. A finding is marked \u003ccode\u003everified_inactive\u003c/code\u003e only when the provider response is definitive under that verifier's contract. Permission denial and ambiguous responses remain \u003ccode\u003everify_error\u003c/code\u003e; for example, SendGrid treats only HTTP \u003ccode\u003e401\u003c/code\u003e as inactive, while \u003ccode\u003e403\u003c/code\u003e remains inconclusive.\u003c/p\u003e\n\u003ch3 id=\"trusted-or-companion-context-required\"\u003eTrusted or companion context required\u003c/h3\u003e\n\u003cp\u003eNine registered implementations cannot safely make a live request from a bare detector finding. Auth0, GitLab, Grafana, GitHub/GHES, Datadog, and Snyk require a trusted issuer/site/API origin. Twilio findings already contain the explicitly paired API Key Secret and non-secret Key SID, but still require a trusted regional origin. Shopify requires an operator-trusted issuing-store origin; finding metadata cannot select it. Supply these targets only with the repeatable \u003ccode\u003e--verifier-origin detector-id=https://host\u003c/code\u003e command-line flag (\u003ccode\u003e--grafana-instance-url\u003c/code\u003e is also retained for Grafana). Project configuration, environment variables, repository URLs, and token claims cannot set a verification target. Without explicit context Leakwatch sends no request and returns \u003ccode\u003eunverified\u003c/code\u003e.\u003c/p\u003e\n\u003ch3 id=\"format-validation-only\"\u003eFormat validation only\u003c/h3\u003e\n\u003cp\u003eFor six credential types, no safe live check exists — the provider has no anonymous identity endpoint, a real call would have side effects, or (for \u003ccode\u003ecoinbase-api-key\u003c/code\u003e) the live API requires HMAC request signing with a paired secret that cannot be reliably associated with the key. For these, Leakwatch validates the structure of the credential without making any network request:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eDetector ID\u003c/th\u003e\n\u003cth\u003eWhat is validated\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egcp-service-account\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eJSON structure — \u003ccode\u003etype\u003c/code\u003e, \u003ccode\u003eproject_id\u003c/code\u003e, \u003ccode\u003eprivate_key_id\u003c/code\u003e, \u003ccode\u003eclient_email\u003c/code\u003e fields present\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003erabbitmq-connection-string\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAMQP URL parsed successfully\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esnowflake-credentials\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFormat check only — a valid format proves nothing, result is always \u003ccode\u003eunverified\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eazure-storage-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFormat check\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eazure-entra-secret\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFormat check\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecoinbase-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCharacter-set and length check\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNote\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eEven when the format check passes, the result remains \u003ccode\u003eunverified\u003c/code\u003e. A structurally valid credential may be expired or revoked. These findings always require manual triage.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"verification-statuses\"\u003eVerification statuses\u003c/h2\u003e\n\u003cp\u003eEvery finding in Leakwatch output carries one of four statuses:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eStatus\u003c/th\u003e\n\u003cth\u003eMeaning\u003c/th\u003e\n\u003cth\u003eRecommended action\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003everified_active\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eThe secret was confirmed live by the provider.\u003c/td\u003e\n\u003ctd\u003eTreat as an active incident. Rotate immediately.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003everified_inactive\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eThe provider rejected the credential.\u003c/td\u003e\n\u003ctd\u003eLikely already rotated. Review context and close.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eunverified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNo verifier exists, required context is absent, a format-only verifier cannot prove liveness, or verification was disabled.\u003c/td\u003e\n\u003ctd\u003eTriage manually; context determines risk.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003everify_error\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eThe verifier ran but encountered a network error, timeout, or unexpected response.\u003c/td\u003e\n\u003ctd\u003eTreat as potentially active. Retry or triage manually.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"the-verification-engine\"\u003eThe verification engine\u003c/h2\u003e\n\u003cp\u003eVerification runs in a dedicated concurrent worker pool, isolated from the scan worker pool. The defaults are conservative to avoid triggering provider rate limits:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eSetting\u003c/th\u003e\n\u003cth\u003eDefault\u003c/th\u003e\n\u003cth\u003eConfig key\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003eWorker count\u003c/td\u003e\n\u003ctd\u003e4\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003everification.concurrency\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eGlobal + per-detector request ceilings\u003c/td\u003e\n\u003ctd\u003e10 requests/second each\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003everification.rate-limit\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003ePer-finding verification-operation timeout\u003c/td\u003e\n\u003ctd\u003e10 s\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003everification.timeout\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eAll three values are tunable under the \u003ccode\u003everification:\u003c/code\u003e block in \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003everification:\n enabled: true\n concurrency: 4\n rate-limit: 10.0 # requests per second (global and per detector)\n timeout: 10s\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eTip\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eFor repositories with hundreds of findings, keep the provider-safe rate ceiling and reduce \u003ccode\u003econcurrency\u003c/code\u003e if you need gentler traffic. \u003ccode\u003e--only-verified\u003c/code\u003e changes only output filtering; it does not reduce verification requests.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003cp\u003eAdmission occurs immediately before an actual provider request. Format-only and missing-context results do not consume limiter capacity. The per-detector bucket constrains that detector's own request rate, while the shared global bucket constrains aggregate traffic; this does not guarantee fair scheduling between providers.\u003c/p\u003e\n\u003ch2 id=\"controlling-verification-at-the-command-line\"\u003eControlling verification at the command line\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eDisable verification entirely\u003c/strong\u003e with \u003ccode\u003e--no-verify\u003c/code\u003e (or set \u003ccode\u003everification.enabled: false\u003c/code\u003e in config). Every finding passes through as \u003ccode\u003eunverified\u003c/code\u003e. Use this for offline or air-gapped environments, or when you want the fastest possible scan without touching any provider API.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --no-verify\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003cstrong\u003eShow only confirmed-live secrets\u003c/strong\u003e with \u003ccode\u003e--only-verified\u003c/code\u003e. Everything that is not \u003ccode\u003everified_active\u003c/code\u003e is dropped from the output. This is the fastest way to triage a large result set — you see only the keys you must act on now.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git . --only-verified\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-warn\"\u003e\u003cdiv class=\"callout-label\"\u003eWarning\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003e\u003ccode\u003e--only-verified\u003c/code\u003e silently drops \u003ccode\u003eunverified\u003c/code\u003e and \u003ccode\u003everify_error\u003c/code\u003e findings. Do not use it as your sole filter in a compliance context — some credential types (JWTs, generic API keys, private keys) can never be verified and would always be excluded.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"secret-safety\"\u003eSecret safety\u003c/h2\u003e\n\u003cp\u003eVerification is designed so that the raw secret value never leaves the process boundary in an unsafe way:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eVerifiers pass the secret directly to the provider's HTTP endpoint over TLS — it is never written to disk, emitted to a log, or cached between runs.\u003c/li\u003e\n\u003cli\u003eA verifier that fails to initialise or encounters a panic is caught by the engine, which marks the finding \u003ccode\u003everify_error\u003c/code\u003e and continues rather than crashing the scan.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/verification/verification-coverage\"\u003eVerification Coverage\u003c/a\u003e — which detector types are live-verified, format-validated, or not verifiable at all.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eConfiguration: Config File\u003c/a\u003e — full reference for the \u003ccode\u003everification:\u003c/code\u003e block.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/output/output-formats\"\u003eOutput Formats\u003c/a\u003e — how the verification status appears in JSON, SARIF, CSV, and table output.\u003c/li\u003e\n\u003c/ul\u003e\n"},"verification/verification-coverage":{"title":"Verification Coverage","description":"Which of the 65 built-in detectors are live-verified, context-required, format-only, or not verifiable — and what that means for triage.","html":"\u003ch1 id=\"verification-coverage\"\u003eVerification Coverage\u003c/h1\u003e\n\u003cp\u003eLeakwatch ships \u003cstrong\u003e65 built-in detectors\u003c/strong\u003e and 54 registered verifier implementations. Registry presence is not the same as live capability: \u003cstrong\u003e39\u003c/strong\u003e detectors can make a live check in the normal production path, \u003cstrong\u003e9\u003c/strong\u003e require trusted operator or companion context, \u003cstrong\u003e6\u003c/strong\u003e perform offline format validation only, and \u003cstrong\u003e11\u003c/strong\u003e have no verifier. This page maps every detector to its actual verification contract.\u003c/p\u003e\n\u003ch2 id=\"live-verified-39-detector-types\"\u003eLive-verified (39 detector types)\u003c/h2\u003e\n\u003cp\u003eFor these types, Leakwatch can make a controlled, non-destructive provider check in the normal production path. A contract-valid success can return \u003ccode\u003everified_active\u003c/code\u003e; only a definitive authentication rejection on the correct issuer can return \u003ccode\u003everified_inactive\u003c/code\u003e. Ambiguous responses remain \u003ccode\u003everify_error\u003c/code\u003e.\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eDetector type\u003c/th\u003e\n\u003cth\u003eProvider\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eaws-access-key-id\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAWS STS (\u003ccode\u003eGetCallerIdentity\u003c/code\u003e)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eslack-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSlack Web API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eopenai-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOpenAI API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eanthropic-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAnthropic API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edeepseek-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDeepSeek API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ehuggingface-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHugging Face API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esendgrid-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSendGrid Web API (\u003ccode\u003e401\u003c/code\u003e is inactive; \u003ccode\u003e403\u003c/code\u003e, including permission denial, remains \u003ccode\u003everify_error\u003c/code\u003e)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003emailgun-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMailgun API (auto-detects and calls the correct EU vs. US regional endpoint)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epostmark-server-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePostmark API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003estripe-api-key-live\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eStripe API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003estripe-api-key-test\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eStripe API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edigitalocean-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDigitalOcean API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecloudflare-api-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCloudflare API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eheroku-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHeroku Platform API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003evercel-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eVercel REST API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003enpm-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003enpm Registry API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epypi-api-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePyPI API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003erubygems-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRubyGems API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edockerhub-pat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDocker Hub API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecircleci-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCircleCI API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eterraform-cloud-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTerraform Cloud API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ediscord-bot-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDiscord API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003etelegram-bot-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTelegram Bot API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esentry-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSentry API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epagerduty-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePagerDuty API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003enewrelic-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNew Relic NerdGraph (bounded official US/EU fallback; inactive only when both regions return \u003ccode\u003e401\u003c/code\u003e)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edoppler-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDoppler API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003elaunchdarkly-sdk-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eLaunchDarkly API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esonarcloud-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSonarCloud API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003enotion-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNotion API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003elinear-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eLinear API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003efigma-pat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFigma REST API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eairtable-pat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAirtable API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eokta-api-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOkta API (targets the org domain captured alongside the token)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edatabricks-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDatabricks REST API (calls the workspace host captured alongside the token)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ebitbucket-app-password\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBitbucket REST API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esupabase-service-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSupabase Management API (\u003ccode\u003esbp_\u003c/code\u003e personal access token; \u003ccode\u003e401\u003c/code\u003e is inactive, \u003ccode\u003e403\u003c/code\u003e remains \u003ccode\u003everify_error\u003c/code\u003e)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003einfura-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInfura API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eteams-webhook\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMicrosoft Teams\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"requires-trusted-or-companion-context-9-detector-types\"\u003eRequires trusted or companion context (9 detector types)\u003c/h2\u003e\n\u003cp\u003eThese implementations are registered, but a bare detector finding is not enough to choose a safe issuer or authenticate the verification request. When the required context is absent, Leakwatch makes no unsafe guess and returns \u003ccode\u003eunverified\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003eSupply an origin only through the repeatable command-line flag \u003ccode\u003e--verifier-origin detector-id=https://host\u003c/code\u003e. This routing input is intentionally ignored in project configuration files and environment variables, so scanned content cannot redirect credentials. The older \u003ccode\u003e--grafana-instance-url\u003c/code\u003e flag remains a Grafana-only alias.\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eDetector ID\u003c/th\u003e\n\u003cth\u003eRequired context\u003c/th\u003e\n\u003cth\u003eProduction behavior\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eauth0-management-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOperator-trusted Auth0 tenant or custom-domain origin\u003c/td\u003e\n\u003ctd\u003eConfigure with \u003ccode\u003e--verifier-origin auth0-management-token=https://tenant\u003c/code\u003e. The detector emits a complete three-segment Management JWT, but unverified JWT claims and repository URLs never select a request target. The verifier uses a read-only clients probe and only \u003ccode\u003e401\u003c/code\u003e is inactive.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egitlab-pat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOperator-trusted GitLab.com or self-managed API origin\u003c/td\u003e\n\u003ctd\u003eConfigure with \u003ccode\u003e--verifier-origin gitlab-pat=https://gitlab.example\u003c/code\u003e. Repository content and finding metadata never select the request target. Only \u003ccode\u003eglpat-\u003c/code\u003e personal access tokens have the safe \u003ccode\u003e/api/v4/user\u003c/code\u003e probe; all other recognized GitLab token subtypes remain \u003ccode\u003eunverified\u003c/code\u003e. After active identity proof, a best-effort \u003ccode\u003e/personal_access_tokens/self\u003c/code\u003e call adds bounded, validated, sorted standard or GitLab 19.2+ granular scopes and ISO expiry to \u003ccode\u003everification.extra_data\u003c/code\u003e; unavailable or malformed metadata never erases the active proof. A \u003ccode\u003e401\u003c/code\u003e is inactive only when its strict JSON body is GitLab's standard invalid-token response; DPoP challenges remain \u003ccode\u003everify_error\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egrafana-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTrusted Grafana instance origin supplied with \u003ccode\u003e--grafana-instance-url\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCalls only the validated HTTPS instance. Repository content and finding metadata cannot choose the target; \u003ccode\u003e401\u003c/code\u003e is inactive only on that trusted issuer.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003etwilio-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePaired API Key SID plus an operator-trusted regional API origin (US1/IE1/AU1)\u003c/td\u003e\n\u003ctd\u003eConfigure with \u003ccode\u003e--verifier-origin twilio-api-key=https://api.twilio.com\u003c/code\u003e (or the correct regional Twilio origin). A bare \u003ccode\u003eSK...\u003c/code\u003e SID is a public identifier and is not reported. The detector emits an opaque, explicitly assigned secret only when it pairs one-to-one with a nearby assigned SID. Only \u003ccode\u003e401\u003c/code\u003e is inactive and permission \u003ccode\u003e403\u003c/code\u003e remains \u003ccode\u003everify_error\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eshopify-access-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOperator-trusted issuing store origin\u003c/td\u003e\n\u003ctd\u003eConfigure with \u003ccode\u003e--verifier-origin shopify-access-token=https://store.myshopify.com\u003c/code\u003e. Finding metadata is never trusted for routing. The verifier uses the pinned 2026-07 Admin GraphQL shop identity query; only \u003ccode\u003e401\u003c/code\u003e on the selected store is inactive.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egithub-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTrusted GitHub.com or GitHub Enterprise Server API origin\u003c/td\u003e\n\u003ctd\u003eConfigure with \u003ccode\u003e--verifier-origin github-token=https://api.github.com\u003c/code\u003e or the GHES origin. GHES uses the same \u003ccode\u003eghp_\u003c/code\u003e and \u003ccode\u003egithub_pat_\u003c/code\u003e formats as GitHub.com; repository metadata cannot choose the issuer. Validated scope/count and expiry response headers are added to \u003ccode\u003everification.extra_data\u003c/code\u003e when the issuer provides them.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egithub-oauth-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTrusted GitHub.com or GitHub Enterprise Server API origin\u003c/td\u003e\n\u003ctd\u003eConfigure with \u003ccode\u003e--verifier-origin github-oauth-token=https://api.github.com\u003c/code\u003e or the GHES origin. \u003ccode\u003egho_\u003c/code\u003e/\u003ccode\u003eghu_\u003c/code\u003e use \u003ccode\u003e/user\u003c/code\u003e and receive the same safe scope/expiry enrichment; \u003ccode\u003eghs_\u003c/code\u003e uses the installation repositories endpoint. \u003ccode\u003eghr_\u003c/code\u003e refresh tokens remain \u003ccode\u003eunverified\u003c/code\u003e without a request because exchanging one rotates it.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edatadog-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTrusted Datadog site/API origin\u003c/td\u003e\n\u003ctd\u003eConfigure the exact official site with \u003ccode\u003e--verifier-origin datadog-api-key=https://api.datadoghq.com\u003c/code\u003e (or its regional/FED host). Arbitrary hosts are rejected; a wrong region's rejection is not used as revocation evidence.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esnyk-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTrusted Snyk regional, government, or private API origin\u003c/td\u003e\n\u003ctd\u003eConfigure with \u003ccode\u003e--verifier-origin snyk-api-key=https://api.snyk.io\u003c/code\u003e (or the correct regional/private origin). A wrong region's rejection is not proof of revocation. Only \u003ccode\u003e401\u003c/code\u003e proves inactivity; \u003ccode\u003e403\u003c/code\u003e remains \u003ccode\u003everify_error\u003c/code\u003e because it can mean a valid token lacks API-plan or endpoint permission.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"format-validated-only-6-detector-types\"\u003eFormat-validated only (6 detector types)\u003c/h2\u003e\n\u003cp\u003eThese verifiers run entirely offline. No network request is made. Because a valid format does not prove a credential is active, all six always return \u003ccode\u003eunverified\u003c/code\u003e regardless of whether the format check passes or fails.\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eDetector ID\u003c/th\u003e\n\u003cth\u003eWhat is validated\u003c/th\u003e\n\u003cth\u003eWhy no live check\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egcp-service-account\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eJSON structure (\u003ccode\u003etype\u003c/code\u003e, \u003ccode\u003eproject_id\u003c/code\u003e, \u003ccode\u003eprivate_key_id\u003c/code\u003e, \u003ccode\u003eclient_email\u003c/code\u003e)\u003c/td\u003e\n\u003ctd\u003eLive check requires a GCP OAuth2 token exchange, which has side effects\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003erabbitmq-connection-string\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAMQP URL parsed successfully\u003c/td\u003e\n\u003ctd\u003eNo public unauthenticated health endpoint\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esnowflake-credentials\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePassword length and host substring check\u003c/td\u003e\n\u003ctd\u003eLive check requires a JDBC/ODBC database connection\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eazure-storage-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFormat check\u003c/td\u003e\n\u003ctd\u003eRequires per-account HMAC signing; no generic identity endpoint\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eazure-entra-secret\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFormat check\u003c/td\u003e\n\u003ctd\u003eClient credential flow would create sessions\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecoinbase-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCharacter-set and length check\u003c/td\u003e\n\u003ctd\u003eCoinbase's legacy API authenticates with HMAC-SHA256 request signing that requires the paired secret, which the detector cannot reliably associate with the key; live verification is not attempted so a real key is never misreported as inactive\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"not-verifiable-11-detector-types\"\u003eNot verifiable (11 detector types)\u003c/h2\u003e\n\u003cp\u003eThese detector types have no verifier at all. Findings from them are always \u003ccode\u003eunverified\u003c/code\u003e. This is \u003cstrong\u003enot\u003c/strong\u003e because they are unimportant — they are detected and reported in full — but because no public verification API exists, or because any verification attempt would have side effects.\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eDetector ID\u003c/th\u003e\n\u003cth\u003eReason\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ejwt\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eA JWT can be issued by any party; there is no universal validation endpoint\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eprivate-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNo provider to call; active use cannot be detected remotely\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egeneric-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eUnknown provider by definition\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edatabase-connection-string\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eConnecting would create sessions on the target database\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eredis-connection-string\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eConnecting would open a live connection to the Redis instance\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eftp-credentials\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNo safe read-only FTP probe\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eldap-credentials\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eLDAP bind would create an authenticated session\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eslack-webhook\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eConfirming a webhook is active requires sending a message\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ehashicorp-vault-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eVault token validation requires knowing the Vault endpoint\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ediscord-webhook-url\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eConfirming a webhook is active requires posting a message to it\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003estructured-config-secret\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eContextual fallback can identify a secret role but not the credential provider or issuer\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNote\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003e\u0026quot;Not verifiable\u0026quot; does not mean \u0026quot;not found\u0026quot;. All 11 of these types are still detected and appear in your output. They require manual triage to determine whether the credential is live and whether it needs rotation.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"coverage-summary\"\u003eCoverage summary\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eCategory\u003c/th\u003e\n\u003cth\u003eCount\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003eLive-verified\u003c/td\u003e\n\u003ctd\u003e39\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eRequires trusted/companion context\u003c/td\u003e\n\u003ctd\u003e9\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eFormat-validated only\u003c/td\u003e\n\u003ctd\u003e6\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eNot verifiable\u003c/td\u003e\n\u003ctd\u003e11\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eTotal detectors\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003e\u003cstrong\u003e65\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eRegistered verifier implementations\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003e\u003cstrong\u003e54 (83.1%)\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"see-also\"\u003eSee also\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eHow Verification Works\u003c/a\u003e — the two verification modes, statuses, and the verification engine.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/detectors/detector-catalog\"\u003eDetector Catalog\u003c/a\u003e — the full list of built-in detectors with severities.\u003c/li\u003e\n\u003c/ul\u003e\n"}}; diff --git a/site/js/manuals/tr.js b/site/js/manuals/tr.js index 873c35a..df1cad7 100644 --- a/site/js/manuals/tr.js +++ b/site/js/manuals/tr.js @@ -1,3 +1,3 @@ // Generated by tools/site-build. Do not edit by hand. window.LW_MANUAL = window.LW_MANUAL || {}; -window.LW_MANUAL["tr"] = {"ci-cd/docker-usage":{"title":"Docker Kullanımı","description":"Resmi Docker imajını kullanarak Leakwatch taramalarını bir konteyner içinde çalıştırın.","html":"\u003ch1 id=\"docker-kullanm\"\u003eDocker Kullanımı\u003c/h1\u003e\n\u003cp\u003eResmi Leakwatch konteyner imajı, ana makineye herhangi bir şey kurmadan tarama yapmanızı sağlar. İmaj \u003ccode\u003eCGO_ENABLED=0\u003c/code\u003e ile statik olarak derlenmiş ve root olmayan bir kullanıcı olarak çalışır; bu nedenle kilitli CI ortamlarında ve ana sistemi değiştirmek istemediğiniz paylaşımlı makinelerde güvenle kullanılabilir.\u003c/p\u003e\n\u003ch2 id=\"maj-referans\"\u003eİmaj referansı\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eghcr.io/hodetech/leakwatch\n\u003c/code\u003e\u003c/pre\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eEtiket\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e:latest\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eEn son sürüm\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e:v1.5.0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTam sürüm sabitleme\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e:v1.5\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eKüçük sürüm sabitleme (yama sürümlerini takip eder)\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eİmaj Alpine tabanlıdır, root olmayan \u003ccode\u003eleakwatch\u003c/code\u003e kullanıcısı olarak çalışır, çalışma dizini olarak \u003ccode\u003e/scan\u003c/code\u003e kullanır ve giriş noktası olarak \u003ccode\u003eleakwatch\u003c/code\u003e'ı ayarlar.\u003c/p\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNot\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eGiriş noktası \u003ccode\u003eleakwatch\u003c/code\u003e olduğundan alt komutu ve bayrakları doğrudan imaj adının ardına eklersiniz — örneğin \u003ccode\u003eghcr.io/hodetech/leakwatch:latest scan fs /scan\u003c/code\u003e. İkili dosya adını tekrar yazmanıza gerek yoktur.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"yerel-dizin-tarama\"\u003eYerel dizin tarama\u003c/h2\u003e\n\u003cp\u003eTaramak istediğiniz dizini konteyner içindeki \u003ccode\u003e/scan\u003c/code\u003e dizinine bağlayın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003edocker run --rm \\\n -v \u0026quot;$(pwd):/scan\u0026quot; \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan fs /scan\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eAna makinedeki bir dosyaya sonuç yazmak için çıktı dosyasını bağlı birime yazın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003edocker run --rm \\\n -v \u0026quot;$(pwd):/scan\u0026quot; \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan fs /scan --format sarif -o /scan/leakwatch.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003eleakwatch.sarif\u003c/code\u003e dosyası, konteyner çıktıktan sonra ana makinedeki geçerli dizinde görünür.\u003c/p\u003e\n\u003ch2 id=\"uzak-git-deposu-tarama\"\u003eUzak Git deposu tarama\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003edocker run --rm \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan git https://github.com/org/repo.git --format json\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eUzak Git depoları için birim bağlaması gerekli değildir — Leakwatch bunları konteyner içindeki geçici bir dizine klonlar.\u003c/p\u003e\n\u003ch2 id=\"konteyner-imaj-tarama\"\u003eKonteyner imajı tarama\u003c/h2\u003e\n\u003cp\u003eLeakwatch daemonsuz çalışır: imaj katmanlarını Docker daemon'ına ihtiyaç duymadan doğrudan kayıt defterinden çeker. Bu, Leakwatch konteynerinden, ana makine Docker soketini bağlamadan uzak bir imajı tarayabileceğiniz anlamına gelir:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003edocker run --rm \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan image registry.example.com/my-app:v2.3.0\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eÖzel kayıt defterleri için kimlik bilgilerini, kayıt defterinizin desteklediği standart ortam değişkenleri aracılığıyla geçirin (örneğin, bağlı bir kimlik bilgisi dosyasına işaret eden \u003ccode\u003eDOCKER_CONFIG\u003c/code\u003e).\u003c/p\u003e\n\u003ch2 id=\"yaplandrma-dosyas-geirme\"\u003eYapılandırma dosyası geçirme\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003e.leakwatch.yaml\u003c/code\u003e dosyasını \u003ccode\u003e/scan\u003c/code\u003e dizinine bağlayın; Leakwatch onu otomatik olarak bulur:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003edocker run --rm \\\n -v \u0026quot;$(pwd):/scan\u0026quot; \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan fs /scan\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003e.leakwatch.yaml\u003c/code\u003e bağlanan dizinde olduğu sürece Leakwatch onu bulur çünkü \u003ccode\u003e/scan\u003c/code\u003e hem çalışma dizini hem de taramaya geçirilen yoldur. Yapılandırma dosyanız başka bir yerdeyse onu ayrıca bağlayın ve \u003ccode\u003e--config\u003c/code\u003e kullanın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003edocker run --rm \\\n -v \u0026quot;$(pwd):/scan\u0026quot; \\\n -v \u0026quot;/path/to/custom-config.yaml:/config/leakwatch.yaml:ro\u0026quot; \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan fs /scan --config /config/leakwatch.yaml\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"ortam-deikenleri-geirme\"\u003eOrtam değişkenleri geçirme\u003c/h2\u003e\n\u003cp\u003eBulut taraması ve token tabanlı kimlik doğrulama için ortam değişkenleri \u003ccode\u003e-e\u003c/code\u003e ile enjekte edilebilir:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# AWS kimlik bilgileriyle S3 taraması\ndocker run --rm \\\n -e AWS_ACCESS_KEY_ID=AKIA••••••••••••EXAMPLE \\\n -e AWS_SECRET_ACCESS_KEY=••••••••••••••••••••••••••••••••••••••• \\\n -e AWS_REGION=us-east-1 \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan s3 my-bucket\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eCI ortamlarında, kimlik bilgilerini komut satırına gömmek yerine maskelenmiş CI değişkenleri olarak enjekte etmeyi tercih edin.\u003c/p\u003e\n\u003ch2 id=\"kt-dosyas-kalb\"\u003eÇıktı dosyası kalıbı\u003c/h2\u003e\n\u003cp\u003eCI'da yaygın bir Docker kalıbı, sonuçları bağlı birime yazmak ve ardından dosyayı bir pipeline artifact'i olarak yüklemek veya arşivlemektir:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003edocker run --rm \\\n -v \u0026quot;$(pwd):/scan\u0026quot; \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan fs /scan \\\n --format json \\\n --only-verified \\\n -o /scan/leakwatch-results.json\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/installation\"\u003eKurulum\u003c/a\u003e — Docker kullanmak yerine yerel ikili dosyayı kurma.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/filesystem\"\u003eDosya Sistemi Taraması\u003c/a\u003e — \u003ccode\u003escan fs\u003c/code\u003e bayrakları ve davranışı.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/container-images\"\u003eKonteyner İmajları\u003c/a\u003e — OCI/Docker imaj katmanlarını sır açısından tarama.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/other-ci\"\u003eDiğer CI Sistemleri\u003c/a\u003e — GitLab CI ve diğer pipeline'larda Docker imajını kullanma.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Referansı\u003c/a\u003e — tüm alt komutlar için tam bayrak referansı.\u003c/li\u003e\n\u003c/ul\u003e\n"},"ci-cd/github-action":{"title":"GitHub Action","description":"GitHub iş akışlarında sır taraması yapmak için resmi Leakwatch GitHub Action'ını kullanın.","html":"\u003ch1 id=\"github-action\"\u003eGitHub Action\u003c/h1\u003e\n\u003cp\u003eDeponuza yapılan her push, bir sırrın içeri sızması için bir fırsattır. GitHub Marketplace'te yayımlanan ve \u003ccode\u003eHodeTech/Leakwatch@v1\u003c/code\u003e olarak kullanılan resmi \u003cstrong\u003eLeakwatch GitHub Action\u003c/strong\u003e, Leakwatch'ı doğrudan GitHub iş akışınıza entegre eder. Runner için önceden derlenmiş Leakwatch ikilisini indirir (Go araç zinciri veya derleme adımı gerekmez), taramayı çalıştırır, çıkış kodlarını işler, bir iş özeti (job summary) yazar ve isteğe bağlı olarak SARIF sonuçlarını GitHub Code Scanning'e yükler — bunların hepsini harici bir servis bağımlılığı olmadan yapar.\u003c/p\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNot\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003e\u003cstrong\u003eDesteklenen runner'lar:\u003c/strong\u003e action, Linux (\u003ccode\u003eubuntu-*\u003c/code\u003e) ve macOS (\u003ccode\u003emacos-*\u003c/code\u003e) runner'larında çalışır. Windows runner'ları henüz desteklenmemektedir; taramayı bir Linux/macOS runner'ında çalıştırın veya \u003ccode\u003eghcr.io/hodetech/leakwatch\u003c/code\u003e konteyner imajını kullanın.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"hzl-balang\"\u003eHızlı başlangıç\u003c/h2\u003e\n\u003cp\u003eSır bulunduğunda iş akışını engelleyen minimal yapılandırma:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e# .github/workflows/leakwatch-minimal.yml\nname: Sır taraması (minimal)\n\non: [push, pull_request]\n\njobs:\n leakwatch:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: HodeTech/Leakwatch@v1\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eYalnızca varsayılan değerlerle action, dosya sistemi taraması yapar (\u003ccode\u003escan-type: fs\u003c/code\u003e), SARIF çıktısı üretir, canlı doğrulamayı atlar (\u003ccode\u003eno-verify: true\u003c/code\u003e) ve herhangi bir bulgu raporlandığında işi başarısız kılar.\u003c/p\u003e\n\u003ch2 id=\"sarif-ykleme-ile-tam-rnek\"\u003eSARIF yükleme ile tam örnek\u003c/h2\u003e\n\u003cp\u003eAşağıdaki iş akışı, GitHub Code Scanning'e SARIF yüklemeyi etkinleştirir ve bulguları depo içinde güvenlik uyarıları olarak gösterir:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e# .github/workflows/leakwatch.yml\nname: Sır taraması\n\non:\n push:\n branches: [\u0026quot;main\u0026quot;, \u0026quot;develop\u0026quot;]\n pull_request:\n\npermissions:\n contents: read\n security-events: write # SARIF yüklemesi için gerekli\n\njobs:\n leakwatch:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n\n - name: Sırları tara\n uses: HodeTech/Leakwatch@v1\n with:\n scan-type: fs\n path: .\n format: sarif\n no-verify: \u0026quot;true\u0026quot;\n min-severity: low\n sarif-upload: \u0026quot;true\u0026quot;\n fail-on-findings: \u0026quot;true\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNot\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eSARIF yüklemesi, işin \u003ccode\u003epermissions: security-events: write\u003c/code\u003e bildirmesini gerektirir. Bu olmadan yükleme adımı 403 hatasıyla başarısız olur. \u003ccode\u003eactions/checkout@v4\u003c/code\u003e için \u003ccode\u003econtents: read\u003c/code\u003e izni de gereklidir.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"girdiler\"\u003eGirdiler\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eGirdi\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003escan-type\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efs\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇalıştırılacak tarama türü: \u003ccode\u003efs\u003c/code\u003e, \u003ccode\u003egit\u003c/code\u003e veya \u003ccode\u003eimage\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epath\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e.\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTaranacak yol (\u003ccode\u003efs\u003c/code\u003e/\u003ccode\u003egit\u003c/code\u003e için) veya imaj referansı (\u003ccode\u003eimage\u003c/code\u003e için).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eformat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003esarif\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktı biçimi: \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e veya \u003ccode\u003egithub\u003c/code\u003e (satır içi pull-request ek açıklamaları).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eoutput\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e``\u003c/td\u003e\n\u003ctd\u003eBiçimlendirilmiş çıktıyı bu dosyaya yaz (\u003ccode\u003eworking-directory\u003c/code\u003e'ye göreli). \u003ccode\u003eformat: github\u003c/code\u003e için yok sayılır. Boş ve \u003ccode\u003eformat: sarif\u003c/code\u003e ise varsayılan \u003ccode\u003eresults.sarif\u003c/code\u003e'tir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eonly-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYalnızca canlı doğrulama ile etkin olduğu teyit edilen bulguları raporla.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eno-verify\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003etrue\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSır doğrulamasını devre dışı bırak (sağlayıcılara giden ağ çağrısı yapılmaz).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003emin-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRaporlanacak minimum önem derecesi: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e veya \u003ccode\u003ecritical\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eremediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktıya giderme (remediation) rehberi ekle.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003econfig\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e``\u003c/td\u003e\n\u003ctd\u003eBir \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e yapılandırma dosyasının yolu.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003escan-diff\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eauto\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003egit\u003c/code\u003e taramalarında yalnızca olaya yeni gelen commit'leri tara. \u003ccode\u003eauto\u003c/code\u003e, bunu \u003ccode\u003epull_request\u003c/code\u003e/\u003ccode\u003epush\u003c/code\u003e olaylarında etkinleştirir; \u003ccode\u003etrue\u003c/code\u003e zorlar; \u003ccode\u003efalse\u003c/code\u003e her zaman tüm geçmişi tarar. \u003ccode\u003eactions/checkout\u003c/code\u003e ile \u003ccode\u003efetch-depth: 0\u003c/code\u003e gerektirir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eextra-args\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e``\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eleakwatch scan\u003c/code\u003e komutuna eklenen ek ham argümanlar (boşlukla ayrılmış). Action'ın kendi yönettiği bayraklar (\u003ccode\u003e--format\u003c/code\u003e, \u003ccode\u003e--output\u003c/code\u003e, \u003ccode\u003e--config\u003c/code\u003e, \u003ccode\u003e--show-raw\u003c/code\u003e) reddedilir — bunun yerine ilgili input'ları kullanın.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eworking-directory\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e.\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTaramanın çalıştırılacağı dizin.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esarif-upload\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTaramadan sonra SARIF sonuçlarını GitHub Code Scanning'e yükle.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003efail-on-findings\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003etrue\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBulgular raporlandığında (çıkış kodu 1) iş akışı adımını başarısız kıl. \u003ccode\u003efalse\u003c/code\u003e olarak ayarlandığında adım başarısız olmak yerine \u003ccode\u003e::warning::\u003c/code\u003e ek açıklaması yayar. Ciddi hatalar (çıkış kodu ≥ 2) bu ayardan bağımsız olarak her zaman adımı başarısız kılar.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eversion\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elatest\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eKurulacak Leakwatch sürümü: \u003ccode\u003elatest\u003c/code\u003e veya belirli bir sürümü sabitlemek için \u003ccode\u003ev1.5.0\u003c/code\u003e gibi bir etiket.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003erelease-repo\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eHodeTech/Leakwatch\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSürüm ikilisinin indirileceği depo (\u003ccode\u003eowner/name\u003c/code\u003e). Yalnızca fork veya kendi sunucunuzdaki aynalar için değiştirin.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"ktlar\"\u003eÇıktılar\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eÇıktı\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003efindings-count\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBulgu raporlanmadıysa \u003ccode\u003e0\u003c/code\u003e; bulgu raporlandıysa \u003ccode\u003e1\u003c/code\u003e. Leakwatch çıkış kodunu yansıtır.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esarif-file\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRunner üzerindeki SARIF çıktı dosyasının yolu (\u003ccode\u003eformat: sarif\u003c/code\u003e olduğunda ayarlanır).\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"cida-dorulama\"\u003eCI'da doğrulama\u003c/h2\u003e\n\u003cp\u003eVarsayılan olarak \u003ccode\u003eno-verify\u003c/code\u003e değeri \u003ccode\u003etrue\u003c/code\u003e'dur — CI'da canlı doğrulama \u003cstrong\u003ekapalıdır\u003c/strong\u003e. Bu, taramayı hızlı tutar ve CI runner'larından sağlayıcı API'lerine giden ağ çağrılarını önler; runner'lar güvenlik duvarı arkasında olabilir veya hız sınırlı kimlik bilgilerine sahip olabilir.\u003c/p\u003e\n\u003cp\u003eCI'da doğrulamayı etkinleştirmek için \u003ccode\u003eno-verify: \u0026quot;false\u0026quot;\u003c/code\u003e olarak ayarlayın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e- uses: HodeTech/Leakwatch@v1\n with:\n no-verify: \u0026quot;false\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-warn\"\u003e\u003cdiv class=\"callout-label\"\u003eUyarı\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eCI'da doğrulamayı etkinleştirmek, Leakwatch'ın her aday bulgu için sağlayıcılara (AWS, GitHub, Stripe vb.) kimlik doğrulamalı API çağrıları yapmasına neden olur. Sağlayıcı hız limitlerinden haberdar olun ve runner'ın giden internet erişimine sahip olduğundan emin olun.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"sarif-yklemesi-nasl-alr\"\u003eSARIF yüklemesi nasıl çalışır\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003esarif-upload: \u0026quot;true\u0026quot;\u003c/code\u003e ve \u003ccode\u003eformat: sarif\u003c/code\u003e olduğunda action:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eLeakwatch'a çıktıyı \u003ccode\u003eresults.sarif\u003c/code\u003e dosyasına yazmasını söyler.\u003c/li\u003e\n\u003cli\u003eTaramanın ardından \u003ccode\u003ecategory: leakwatch\u003c/code\u003e ile \u003ccode\u003egithub/codeql-action/upload-sarif@v3\u003c/code\u003e'ü çağırır.\u003c/li\u003e\n\u003cli\u003eGitHub dosyayı işler ve bulguları deponun \u003cstrong\u003eSecurity\u003c/strong\u003e sekmesinde \u003cstrong\u003eCode Scanning uyarıları\u003c/strong\u003e olarak gösterir.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eYükleme adımı \u003ccode\u003eif: always()\u003c/code\u003e ile çalışır; dolayısıyla \u003ccode\u003efail-on-findings: \u0026quot;true\u0026quot;\u003c/code\u003e tarama adımını başarısız kılsa bile sonuçlar yüklenir.\u003c/p\u003e\n\u003ch2 id=\"action-ktlarn-kullanmak\"\u003eAction çıktılarını kullanmak\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e- name: Sırları tara\n id: scan\n uses: HodeTech/Leakwatch@v1\n with:\n fail-on-findings: \u0026quot;false\u0026quot; # iş akışının devam etmesine izin ver\n\n- name: Sonucu yazdır\n run: echo \u0026quot;Raporlanan bulgular: ${{ steps.scan.outputs.findings-count }}\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"belirli-bir-srm-sabitleme\"\u003eBelirli bir sürümü sabitleme\u003c/h2\u003e\n\u003cp\u003eYeniden üretilebilir derlemeler için \u003ccode\u003eversion\u003c/code\u003e değerini belirli bir etikete sabitleyin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e- uses: HodeTech/Leakwatch@v1\n with:\n version: \u0026quot;v1.5.0\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBu, önceden derlenmiş \u003ccode\u003ev1.5.0\u003c/code\u003e ikilisini \u003ca href=\"https://github.com/HodeTech/Leakwatch/releases\"\u003eLeakwatch sürümlerinden\u003c/a\u003e indirir ve çalıştırmadan önce SHA-256 sağlama toplamını doğrular. En yüksek tedarik zinciri güvenliği için action'ın kendisini de bir commit SHA'sına sabitleyebilirsiniz: \u003ccode\u003euses: HodeTech/Leakwatch@\u0026lt;sha\u0026gt;\u003c/code\u003e.\u003c/p\u003e\n\u003ch2 id=\"yalnzca-deien-kodu-tarama-pull-request-diff\"\u003eYalnızca değişen kodu tarama (pull-request diff)\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003egit\u003c/code\u003e taramalarında action, taramayı bir pull request veya push'un gerçekten getirdiği commit'lerle sınırlayabilir; bu daha hızlıdır ve yalnızca yeni eklenen sırları yüzeye çıkarır. \u003ccode\u003escan-diff\u003c/code\u003e (varsayılan \u003ccode\u003eauto\u003c/code\u003e) ile kontrol edilir ve base commit'in yerel olarak bulunması için tam bir checkout gerektirir:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003ejobs:\n leakwatch:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n with:\n fetch-depth: 0 # PR base commit'inin mevcut olması için gerekli\n - uses: HodeTech/Leakwatch@v1\n with:\n scan-type: git\n path: .\n # scan-diff: auto (varsayılan) — pull_request/push'ta yalnızca base..HEAD taranır\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003epull_request\u003c/code\u003e olayında action \u003ccode\u003egithub.event.pull_request.base.sha\u003c/code\u003e'dan; \u003ccode\u003epush\u003c/code\u003e olayında \u003ccode\u003egithub.event.before\u003c/code\u003e'dan itibaren tarar. Her zaman tüm geçmişi taramak için \u003ccode\u003escan-diff: \u0026quot;false\u0026quot;\u003c/code\u003e, diff modunu zorlamak için \u003ccode\u003escan-diff: \u0026quot;true\u0026quot;\u003c/code\u003e kullanın. \u003ccode\u003escan-diff\u003c/code\u003e'in \u003ccode\u003efs\u003c/code\u003e/\u003ccode\u003eimage\u003c/code\u003e taramalarında etkisi yoktur.\u003c/p\u003e\n\u003ch2 id=\"satr-ii-pull-request-ek-aklamalar\"\u003eSatır içi pull-request ek açıklamaları\u003c/h2\u003e\n\u003cp\u003eBulguları GitHub Actions iş akışı komutları olarak yaymak için \u003ccode\u003eformat: github\u003c/code\u003e ayarlayın; bunlar pull request'in \u003cstrong\u003eFiles changed\u003c/strong\u003e görünümünde ve çalışma günlüğünde satır içi ek açıklamalar olarak görünür:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e- uses: HodeTech/Leakwatch@v1\n with:\n format: github\n fail-on-findings: \u0026quot;false\u0026quot; # isterseniz engellemeden yalnızca ek açıklama yapın\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eEk açıklamalar her zaman yalnızca \u003cstrong\u003emaskelenmiş\u003c/strong\u003e değeri gösterir — ham sır, (çoğu zaman herkese açık olan) PR arayüzüne veya günlüklere asla yazılmaz. Hızlı ve görünür PR geri bildirimi için \u003ccode\u003eformat: github\u003c/code\u003e, bulguları \u003cstrong\u003eSecurity\u003c/strong\u003e sekmesinde Code Scanning uyarıları olarak kaydetmek için \u003ccode\u003esarif-upload: true\u003c/code\u003e ile \u003ccode\u003eformat: sarif\u003c/code\u003e kullanın.\u003c/p\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/output/output-formats\"\u003eÇıktı Biçimleri\u003c/a\u003e — JSON, SARIF, CSV ve tablo çıktısını anlama.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/exit-codes\"\u003eÇıkış Kodları\u003c/a\u003e — çıkış kodlarının tarama sonuçlarıyla nasıl eşleştiği.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eDoğrulama Nasıl Çalışır\u003c/a\u003e — Leakwatch'ın sağlayıcı API'lerini ne zaman ve nasıl çağırdığı.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/pre-commit\"\u003ePre-commit Kancası\u003c/a\u003e — commit edilmeden önce sırları yakalama.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/other-ci\"\u003eDiğer CI Sistemleri\u003c/a\u003e — GitLab CI, Jenkins ve genel kabuk entegrasyonu.\u003c/li\u003e\n\u003c/ul\u003e\n"},"ci-cd/other-ci":{"title":"Diğer CI Sistemleri","description":"Leakwatch'ı GitLab CI, Jenkins, Bitbucket Pipelines ve diğer CI sistemlerine entegre edin.","html":"\u003ch1 id=\"dier-ci-sistemleri\"\u003eDiğer CI Sistemleri\u003c/h1\u003e\n\u003cp\u003eLeakwatch, çalışma zamanı bağımlılığı olmayan tek bir statik ikili dosya olduğundan, kabuk komutu çalıştırabilen herhangi bir CI ortamında çalışır: GitLab CI, Jenkins, Bitbucket Pipelines, CircleCI, Azure DevOps ve diğerleri. Bu sayfada açıklananların ötesinde bu sistemler için yerleşik bir entegrasyon yoktur; kalıp her zaman aynıdır: ikili dosyayı kur, taramayı çalıştır, çıkış koduna göre hareket et.\u003c/p\u003e\n\u003ch2 id=\"cida-leakwatch-kurma\"\u003eCI'da Leakwatch kurma\u003c/h2\u003e\n\u003cp\u003eRunner ortamınıza en uygun yöntemi seçin:\u003c/p\u003e\n\u003ch3 id=\"go-install-araclyla-runnerda-go-gerektirir\"\u003e\u003ccode\u003ego install\u003c/code\u003e aracılığıyla (runner'da Go gerektirir)\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003ego install github.com/HodeTech/leakwatch@latest\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eYeniden üretilebilir derlemeler için belirli bir sürüme sabitleyin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003ego install github.com/HodeTech/leakwatch@v1.5.0\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"docker-imaj-araclyla-go-gerekmez\"\u003eDocker imajı aracılığıyla (Go gerekmez)\u003c/h3\u003e\n\u003cp\u003e\u003ccode\u003eghcr.io/hodetech/leakwatch:latest\u003c/code\u003e'i iş imajı olarak kullanın veya \u003ccode\u003edocker run\u003c/code\u003e ile çalıştırın. Tam kalıp için \u003ca href=\"#/ci-cd/docker-usage\"\u003eDocker Kullanımı\u003c/a\u003e sayfasına bakın.\u003c/p\u003e\n\u003ch3 id=\"hazr-bir-srm-ikili-dosyas-araclyla\"\u003eHazır bir sürüm ikili dosyası aracılığıyla\u003c/h3\u003e\n\u003cp\u003eUygun tar arşivini \u003ca href=\"https://github.com/HodeTech/Leakwatch/releases\"\u003eGitHub Releases\u003c/a\u003e sayfasından indirin, çıkarın ve \u003ccode\u003ePATH\u003c/code\u003e'e ekleyin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003ecurl -LO https://github.com/HodeTech/Leakwatch/releases/latest/download/leakwatch_Linux_amd64.tar.gz\ntar -xzf leakwatch_Linux_amd64.tar.gz\nsudo mv leakwatch /usr/local/bin/leakwatch\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"k-kodlar\"\u003eÇıkış kodları\u003c/h2\u003e\n\u003cp\u003eLeakwatch, bir CI derlemesini başarısız kılmanın birincil mekanizması olan dört koddan biriyle çıkar:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eKod\u003c/th\u003e\n\u003cth\u003eAnlam\u003c/th\u003e\n\u003cth\u003eÖnerilen CI eylemi\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBulgu yok\u003c/td\u003e\n\u003ctd\u003ePipeline aşamasını geç\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSırlar bulundu\u003c/td\u003e\n\u003ctd\u003ePipeline aşamasını başarısız kıl\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCiddi hata (hatalı yapılandırma, okunamaz yol vb.)\u003c/td\u003e\n\u003ctd\u003ePipeline aşamasını başarısız kıl\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTamamlanmadan (\u003ccode\u003eCtrl+C\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e) kesildi, hiçbir bulgu raporlanmadı\u003c/td\u003e\n\u003ctd\u003ePipeline aşamasını başarısız kıl — tarama tamamlanmadığından temiz bir sonuca güvenilemez\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eÇıkış koduna göre dallanma yapan genel bir kabuk parçacığı:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eset +e\nleakwatch scan fs . --format json -o leakwatch.json --no-verify\nEXIT_CODE=$?\nset -e\n\nif [ \u0026quot;$EXIT_CODE\u0026quot; -eq 0 ]; then\n echo \u0026quot;Sır bulunamadı.\u0026quot;\nelif [ \u0026quot;$EXIT_CODE\u0026quot; -eq 1 ]; then\n echo \u0026quot;Sırlar bulundu — derlemeyi başarısız kılıyorum.\u0026quot;\n exit 1\nelse\n echo \u0026quot;Tarama hatası veya kesintisi (çıkış $EXIT_CODE) — derlemeyi başarısız kılıyorum.\u0026quot;\n exit \u0026quot;$EXIT_CODE\u0026quot;\nfi\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"gitlab-ci-rnei\"\u003eGitLab CI örneği\u003c/h2\u003e\n\u003cp\u003eAşağıdaki \u003ccode\u003e.gitlab-ci.yml\u003c/code\u003e işi Leakwatch'ı kurar, dosya sistemi taraması çalıştırır ve JSON raporunu pipeline artifact'i olarak saklar:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003eleakwatch:\n stage: test\n image: golang:1.25-alpine\n script:\n - go install github.com/HodeTech/leakwatch@v1.5.0\n - leakwatch scan fs . --format json -o leakwatch.json --no-verify\n artifacts:\n when: always\n paths:\n - leakwatch.json\n expire_in: 7 days\n allow_failure: false\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003eallow_failure: false\u003c/code\u003e (varsayılan) değeri, çıkış kodu \u003ccode\u003e1\u003c/code\u003e'in pipeline aşamasını başarısız kılması anlamına gelir. Taramanın merge işlemini engellemeden raporlamasını istiyorsanız \u003ccode\u003eallow_failure: true\u003c/code\u003e olarak ayarlayın.\u003c/p\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eİpucu\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eGitLab, SAST raporu artifact'larını destekler. Leakwatch SARIF üretir (\u003ccode\u003e--format sarif\u003c/code\u003e) ancak GitLab'ın yerel SAST JSON şemasını değil; bu nedenle \u003ccode\u003ereports: sast:\u003c/code\u003e anahtarı yerine \u003ccode\u003epaths:\u003c/code\u003e artifact yaklaşımını kullanın.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"ci-runnerlar-iin-neriler\"\u003eCI runner'ları için öneriler\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eGiden internet erişimi olmayan runner'larda \u003ccode\u003e--no-verify\u003c/code\u003e kullanın.\u003c/strong\u003e Doğrulama, sağlayıcılara (AWS, GitHub, Stripe vb.) canlı API çağrıları yapar. Hava boşluklu veya güvenlik duvarıyla kısıtlanmış runner'larda bu çağrılar zaman aşımına uğrar ve taramayı yavaşlatır. Doğrulamayı tamamen atlamak için \u003ccode\u003e--no-verify\u003c/code\u003e geçirin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --no-verify --format sarif -o results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003cstrong\u003eÇıktıyı artifact olarak kaydedin.\u003c/strong\u003e İşi tamamlandıktan sonra saklanabilecek, bir güvenlik açığı yönetim platformuna yüklenebilecek veya incelenebilecek bir dosya yazmak için \u003ccode\u003e--format sarif\u003c/code\u003e ya da \u003ccode\u003e--format json\u003c/code\u003e ile birlikte \u003ccode\u003e--output\u003c/code\u003e kullanın.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/strong\u003e değerini en çok önem taşıyan sırlara odaklanmak için ayarlayın. Gürültülü bir kod tabanında \u003ccode\u003e--min-severity high\u003c/code\u003e ile başlayın ve birikmiş öğeleri temizledikten sonra eşiği düşürün.\u003c/p\u003e\n\u003ch2 id=\"azure-devops-rnei\"\u003eAzure DevOps örneği\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e- script: |\n go install github.com/HodeTech/leakwatch@v1.5.0\n leakwatch scan fs . --format sarif -o $(Build.ArtifactStagingDirectory)/leakwatch.sarif --no-verify\n displayName: \u0026quot;Leakwatch sır taraması\u0026quot;\n\n- task: PublishBuildArtifacts@1\n inputs:\n pathToPublish: \u0026quot;$(Build.ArtifactStagingDirectory)\u0026quot;\n artifactName: \u0026quot;leakwatch-results\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"jenkins-rnei\"\u003eJenkins örneği\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-groovy\"\u003estage('Sır taraması') {\n steps {\n sh '''\n go install github.com/HodeTech/leakwatch@v1.5.0\n leakwatch scan fs . --format json -o leakwatch.json --no-verify\n '''\n archiveArtifacts artifacts: 'leakwatch.json', allowEmptyArchive: true\n }\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/reference/exit-codes\"\u003eÇıkış Kodları\u003c/a\u003e — tüm çıkış kodlarının tam referansı.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/output/output-formats\"\u003eÇıktı Biçimleri\u003c/a\u003e — JSON, SARIF, CSV ve tablo çıktısı.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/docker-usage\"\u003eDocker Kullanımı\u003c/a\u003e — ikili dosyayı kurmak yerine konteyner imajını kullanma.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/github-action\"\u003eGitHub Action\u003c/a\u003e — GitHub iş akışları için resmi action.\u003c/li\u003e\n\u003c/ul\u003e\n"},"ci-cd/pre-commit":{"title":"Pre-commit Kancası","description":"Her commit'ten önce sır taraması yapmak için Leakwatch pre-commit kancasını kullanın.","html":"\u003ch1 id=\"pre-commit-kancas\"\u003ePre-commit Kancası\u003c/h1\u003e\n\u003cp\u003eBir sırrı yakalamak için en ucuz an, onu depoya girmeden önce durdurmaktır. Leakwatch, her \u003ccode\u003egit commit\u003c/code\u003e işleminde \u003ccode\u003eleakwatch scan fs\u003c/code\u003e komutunu otomatik olarak çalıştıran yerel bir \u003ca href=\"https://pre-commit.com\"\u003epre-commit\u003c/a\u003e kancası sunar; böylece sızan bir API anahtarı veya parola, geçmişte yer almak yerine commit işlemini başarısız kılar.\u003c/p\u003e\n\u003ch2 id=\"n-koullar\"\u003eÖn koşullar\u003c/h2\u003e\n\u003cp\u003eŞunlara ihtiyacınız var:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003ePython 3.8+ (pre-commit bir Python aracıdır).\u003c/li\u003e\n\u003cli\u003eGenel olarak kurulmuş \u003ca href=\"https://pre-commit.com/#install\"\u003epre-commit\u003c/a\u003e (\u003ccode\u003epip install pre-commit\u003c/code\u003e veya \u003ccode\u003ebrew install pre-commit\u003c/code\u003e).\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ePATH\u003c/code\u003e üzerinde Go 1.25+ — kanca dili \u003ccode\u003egolang\u003c/code\u003e olduğundan pre-commit, ilk çalıştırmada Leakwatch'ı kaynaktan derler.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"yaplandrma\"\u003eYapılandırma\u003c/h2\u003e\n\u003cp\u003eDeponuzun köküne bir \u003ccode\u003e.pre-commit-config.yaml\u003c/code\u003e dosyası ekleyin (veya mevcut olanı genişletin):\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003erepos:\n - repo: https://github.com/HodeTech/Leakwatch\n rev: v1.5.0\n hooks:\n - id: leakwatch\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eKancaları yerel Git deposuna kurun:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003epre-commit install\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eHepsi bu kadar. Bundan itibaren her \u003ccode\u003egit commit\u003c/code\u003e işlemi bir dosya sistemi taraması tetikler. Leakwatch herhangi bir sır bulursa commit engellenir ve bulgular terminale yazdırılır.\u003c/p\u003e\n\u003ch2 id=\"elle-altrma\"\u003eElle çalıştırma\u003c/h2\u003e\n\u003cp\u003eTüm depoyu (yalnızca staged dosyaları değil) istediğiniz zaman taramak için:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003epre-commit run --all-files\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eDiğerlerini tetiklemeden yalnızca Leakwatch kancasını çalıştırmak için:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003epre-commit run leakwatch --all-files\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"ek-argmanlar-geirme\"\u003eEk argümanlar geçirme\u003c/h2\u003e\n\u003cp\u003eKancanın varsayılan davranışı, ek bayrak olmadan \u003ccode\u003eleakwatch scan fs\u003c/code\u003e'e karşılık gelir. \u003ccode\u003eargs:\u003c/code\u003e anahtarı aracılığıyla ek argümanlar geçirebilirsiniz:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003erepos:\n - repo: https://github.com/HodeTech/Leakwatch\n rev: v1.5.0\n hooks:\n - id: leakwatch\n args:\n - --only-verified\n - --min-severity\n - high\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBu örnek, yalnızca Leakwatch'ın hâlâ etkin olduğunu doğruladığı yüksek önem dereceli sırları raporlar — yanlış pozitif gürültüsünden kaçınmak isteyen ancak kapsam kaybetmek istemeyen ekipler için uygun katı bir politika.\u003c/p\u003e\n\u003cp\u003eDiğer kullanışlı argümanlar:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003eargs:\n - --no-verify # daha hızlı commit'ler için canlı doğrulamayı atla\n - --min-severity\n - medium # düşük önem dereceli gürültüyü bastır\n - --format\n - table # terminalde insan tarafından okunabilir çıktı\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNot\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eKanca tanımında \u003ccode\u003epass_filenames: false\u003c/code\u003e ayarlandığından kanca, yalnızca mevcut commit için staged dosyaları değil her zaman tam çalışma ağacını tarar. Bu, staged olmayan dosyalarda halihazırda bulunan sırların da tespit edileceğini garanti eder.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"kancann-taradklar\"\u003eKancanın taradıkları\u003c/h2\u003e\n\u003cp\u003eKanca, depo çalışma dizinine karşı \u003ccode\u003eleakwatch scan fs\u003c/code\u003e çalıştırır. CLI ile aynı tespit hattını kullanır: Aho-Corasick ön filtreleme, regex doğrulama, entropi hesaplama ve (\u003ccode\u003e--no-verify\u003c/code\u003e ayarlanmadıkça) canlı doğrulama.\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003e.leakwatch.yaml\u003c/code\u003e'daki yapılandırma otomatik olarak uygulanır — dışlama kalıpları, entropi eşikleri ve doğrulama ayarları, herhangi bir ek kanca yapılandırması olmadan geçerli olur.\u003c/p\u003e\n\u003ch2 id=\"kancay-geici-olarak-atlama\"\u003eKancayı geçici olarak atlama\u003c/h2\u003e\n\u003cp\u003eKancayı çalıştırmadan commit yapmak için (örneğin, maskelenmiş sır içeren bir test sabiti commit edilirken):\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eSKIP=leakwatch git commit -m \u0026quot;chore: test sabiti ekle\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-warn\"\u003e\u003cdiv class=\"callout-label\"\u003eUyarı\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003e\u003ccode\u003eSKIP=leakwatch\u003c/code\u003e kullanmak, o commit için tüm sır taramasını devre dışı bırakır. Yalnızca içeriğin güvenli olduğunu teyit ettiğinizde kullanın; kalıcı bastırmalar için bunun yerine \u003ccode\u003e.leakwatchignore\u003c/code\u003e veya satır içi \u003ccode\u003eleakwatch:ignore\u003c/code\u003e yorumlarını tercih edin.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"kanca-srmn-sabitli-tutma\"\u003eKanca sürümünü sabitli tutma\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003erev:\u003c/code\u003e değerini dal adı yerine belirli bir etikete sabitleyin. Bu, ekipteki tüm geliştiricilerin aynı dedektör setini kullandığını ve kancanın sprint ortasında sessizce yükseltilmediğini garantiler:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003erev: v1.5.0 # sabitle; 'main' veya 'HEAD' kullanmayın\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eGüncellemek için:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003epre-commit autoupdate\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBu komut \u003ccode\u003erev\u003c/code\u003e değerini en son etikete yükseltir ve siz onu commit etmeden önce değişikliği inceleme fırsatı tanır.\u003c/p\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/filesystem\"\u003eDosya Sistemi Taraması\u003c/a\u003e — kancanın çalıştırdığı temel tarama komutu.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e — \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e'da dışlamaları, entropiyi ve doğrulamayı kontrol etme.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/github-action\"\u003eGitHub Action\u003c/a\u003e — GitHub CI'da her push ve pull request'te tarama.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/exit-codes\"\u003eÇıkış Kodları\u003c/a\u003e — çıkış kodlarının tarama sonuçlarıyla nasıl eşleştiği.\u003c/li\u003e\n\u003c/ul\u003e\n"},"configuration/config-file":{"title":"Yapılandırma Dosyası","description":"Leakwatch'ı .leakwatch.yaml ile yapılandırma — tam şema, varsayılanlar, doğrulama kuralları, ortam değişkeni geçersiz kılmaları ve leakwatch init komutu.","html":"\u003ch1 id=\"yaplandrma-dosyas\"\u003eYapılandırma Dosyası\u003c/h1\u003e\n\u003cp\u003eLeakwatch'ın her tarama komutundaki davranışı, \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e adlı tek bir YAML dosyasıyla yönetilir. Bu dosyayı anlamak; eşzamanlılık, doğrulama, çıktı biçimi ve yol filtrelemeyi bir kez ayarlamanızı ve her taramanın bu ayarları otomatik olarak almasını sağlar.\u003c/p\u003e\n\u003ch2 id=\"dosya-kefi\"\u003eDosya keşfi\u003c/h2\u003e\n\u003cp\u003eLeakwatch, yapılandırma dosyasını aşağıdaki sırayla çözer:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003e--config \u0026lt;path\u0026gt;\u003c/code\u003e bayrağı\u003c/strong\u003e — çalışma dizininden bağımsız olarak açık bir yol kullanır.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eGeçerli dizin\u003c/strong\u003e — komutun çalıştırıldığı dizindeki \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAna dizin\u003c/strong\u003e — yedek olarak \u003ccode\u003e~/.leakwatch.yaml\u003c/code\u003e.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eHiçbir dosya bulunamazsa, her ayar için yerleşik varsayılanlar kullanılır. Bir dosya bulunur\u003cstrong\u003esa\u003c/strong\u003e (veya \u003ccode\u003e--config\u003c/code\u003e bir dosyayı işaret ederse) ama geçerli bir YAML olarak ayrıştırılamazsa, bu ölümcül bir hatadır — tarama sessizce varsayılanlara geri dönmez, çünkü bunu yapmak operatörün yapılandırdığı sınırların ötesinde tespit kapsamını sessizce genişletebilir.\u003c/p\u003e\n\u003ch2 id=\"balang-dosyas-oluturma\"\u003eBaşlangıç dosyası oluşturma\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eleakwatch init\u003c/code\u003e komutu, önerilen varsayılanlarla düzenlemeye hazır bir dosya yazar:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch init\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eVarsayılan olarak dosya, geçerli dizindeki \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e konumuna yazılır. Farklı bir yol seçmek için \u003ccode\u003e--output\u003c/code\u003e kullanın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch init --output /etc/leakwatch/.leakwatch.yaml\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eHedef dosya zaten mevcutsa, \u003ccode\u003eleakwatch init\u003c/code\u003e üzerine yazmayı reddeder ve hata vererek çıkar. Üzerine yazmak için \u003ccode\u003e--force\u003c/code\u003e kullanın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch init --force\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"ortam-deikeni-geersiz-klmalar\"\u003eOrtam değişkeni geçersiz kılmaları\u003c/h2\u003e\n\u003cp\u003eHer yapılandırma anahtarı bir ortam değişkeniyle geçersiz kılınabilir. İsimlendirme kuralı şudur:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eÖnek: \u003ccode\u003eLEAKWATCH_\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e.\u003c/code\u003e ve \u003ccode\u003e-\u003c/code\u003e karakterlerini \u003ccode\u003e_\u003c/code\u003e ile değiştirin\u003c/li\u003e\n\u003cli\u003eBüyük harfe çevirin\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eÖrnekler:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eYapılandırma anahtarı\u003c/th\u003e\n\u003cth\u003eOrtam değişkeni\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003escan.concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_SCAN_CONCURRENCY\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003everification.rate-limit\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_VERIFICATION_RATE_LIMIT\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eoutput.format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_OUTPUT_FORMAT\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edetection.entropy.threshold\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_DETECTION_ENTROPY_THRESHOLD\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"ncelik-sras\"\u003eÖncelik sırası\u003c/h2\u003e\n\u003cp\u003eAynı ayar birden fazla yerde belirtildiğinde, en yüksek öncelikli kaynak kazanır:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eKomut satırı bayrağı (en yüksek)\u003c/li\u003e\n\u003cli\u003eOrtam değişkeni\u003c/li\u003e\n\u003cli\u003eYapılandırma dosyası değeri\u003c/li\u003e\n\u003cli\u003eYerleşik varsayılan (en düşük)\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"tam-ema\"\u003eTam şema\u003c/h2\u003e\n\u003cp\u003eAşağıdaki açıklamalı şema, desteklenen her anahtarı, varsayılan değerini ve geçerli aralığını göstermektedir.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e# ── Tarama motoru ─────────────────────────────────────────────────────────────\n\nscan:\n # Eşzamanlı dosya işleme worker sayısı.\n # Varsayılan olarak ana makinedeki mantıksal CPU çekirdeği sayısı kullanılır.\n # \u0026gt;= 1 olmalıdır.\n concurrency: 8\n\n # Taranacak maksimum dosya boyutu (bayt cinsinden). Bu sınırı aşan dosyalar\n # tamamen atlanır. Varsayılan: 10 MB (10485760). \u0026gt;= 1 olmalıdır.\n max-file-size: 10485760\n\n# ── Tespit ────────────────────────────────────────────────────────────────────\n\ndetection:\n entropy:\n # Her aday eşleşme için Shannon entropi hesaplamasını etkinleştirir.\n enabled: true\n\n # Gösterim için kullanılan ve yerleşik generic-api-key dedektörüyle kendi\n # entropy alanına sahip her özel kuralı kapılayan entropi eşiği.\n # Aralık: 0–8. Varsayılan: 4.0.\n # Yerleşik bulgular hakkındaki nota bakın.\n threshold: 4.0\n\n# ── Doğrulama ─────────────────────────────────────────────────────────────────\n\nverification:\n # Sağlayıcı API'lerine karşı canlı doğrulamayı etkinleştirir.\n enabled: true\n\n # İstek başına HTTP zaman aşımı. Doğrulama etkinleştirildiğinde \u0026gt;= 1ms olmalıdır.\n # Süre dizesi kullanın (örn. \u0026quot;10s\u0026quot;, \u0026quot;500ms\u0026quot;) — tam sayı nanosaniye olarak\n # yorumlanır ve doğrulama başarısız olur.\n timeout: 10s\n\n # Eşzamanlı doğrulama worker sayısı. \u0026gt;= 1 olmalıdır.\n concurrency: 4\n\n # Saniyedeki maksimum doğrulama isteği (token-bucket hız sınırlayıcı).\n # \u0026gt; 0 olmalıdır.\n rate-limit: 10.0\n\n# ── Filtreleme ────────────────────────────────────────────────────────────────\n\nfilter:\n # Taramadan hariç tutulacak yollar için glob desenleri.\n # Desteklenen glob stilleri: filepath.Match desenleri, sıfır veya daha fazla\n # yol segmentini kapsayan ** çift yıldız ve herhangi bir derinlikte adlandırılmış\n # dizini eşleştiren sondaki eğik çizgili dir/ desenleri. Her desen hem tam yol\n # hem de temel dosya adına karşı test edilir.\n # Tüm tarama kaynaklarına uygulanır. (`scan slack` hariç her tarama alt\n # komutunda bulunan --exclude bayrağı, bu listenin yerine geçmek yerine\n # çalışma zamanında ona ekleme yapar.)\n # Varsayılan: [] (yerleşik ikili/kilit dosya atlamalarının ötesinde hariç tutma yok).\n exclude-paths:\n - \u0026quot;vendor/**\u0026quot;\n - \u0026quot;node_modules/**\u0026quot;\n - \u0026quot;**/*.min.js\u0026quot;\n - \u0026quot;**/*.min.css\u0026quot;\n - \u0026quot;go.sum\u0026quot;\n - \u0026quot;package-lock.json\u0026quot;\n - \u0026quot;yarn.lock\u0026quot;\n\n # Tamamen devre dışı bırakılacak dedektör ID'leri. Listelenen dedektörlerden\n # gelen bulgular, diğer ayarlardan bağımsız olarak hiçbir zaman üretilmez.\n # Varsayılan: [].\n # Her tarama alt komutunda bulunan --exclude-detectors bayrağı, bu listenin\n # yerine geçmek yerine çalışma zamanında ona ekleme yapar.\n exclude-detectors: []\n\n# ── Çıktı ─────────────────────────────────────────────────────────────────────\n\noutput:\n # Çıktı biçimi. Şunlardan biri: json, sarif, csv, table, github. Varsayılan: json.\n # --format / -f bayrağı bunu çalışma zamanında geçersiz kılar.\n format: json\n\n # Çıktıyı stdout yerine bu dosya yoluna yaz. Varsayılan: \u0026quot;\u0026quot; (stdout).\n # --output / -o bayrağı bunu çalışma zamanında geçersiz kılar. Uzantısız düz\n # bir yol, biçimin kendi uzantısıyla otomatik olarak tamamlanır ve dosya\n # 0600 izinleriyle yazılır. Her zaman stdout'a yazan \u0026quot;github\u0026quot; biçiminde\n # yok sayılır.\n file: \u0026quot;\u0026quot;\n\n # Bu önem seviyesinin altındaki bulguları bırak.\n # Şunlardan biri: low, medium, high, critical. Varsayılan: \u0026quot;\u0026quot; (tümünü göster).\n # --min-severity bayrağı bunu çalışma zamanında geçersiz kılar.\n severity-threshold: \u0026quot;\u0026quot;\n\n # Çıktıda maskelenmemiş sır değerini dahil et.\n # Varsayılan: false. --show-raw bayrağı bunu çalışma zamanında geçersiz kılar.\n show-raw: false\n\n# ── Özel kurallar ─────────────────────────────────────────────────────────────\n\n# Kendi dedektörlerinizi YAML kuralları olarak tanımlayın. Tam kural şeması\n# için özel kurallar sayfasına bakın.\n# custom-rules:\n# - id: \u0026quot;my-internal-token\u0026quot;\n# description: \u0026quot;Internal Service Token\u0026quot;\n# regex: \u0026quot;mycompany_[a-zA-Z0-9]{32}\u0026quot;\n# keywords: [\u0026quot;mycompany_\u0026quot;]\n# severity: critical\ncustom-rules: []\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNot\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003e\u003ccode\u003edetection.entropy.threshold\u003c/code\u003e, bir bulgunun yanında gösterilen entropi değerini kontrol eder ve entropi tabanlı sezgisel yöntemleri tercih eden her dedektörü kapılar — şu anda yalnızca yerleşik \u003ccode\u003egeneric-api-key\u003c/code\u003e dedektörü, artı kendi \u003ccode\u003eentropy\u003c/code\u003e alanını bildiren özel kurallar. Bunlardan birine ait, entropisi eşiğin altına düşen bir eşleşme, olası bir yer tutucu olarak bastırılır. Diğer her (yapısal/biçim-çapalı) yerleşik dedektör — \u003ccode\u003eaws-access-key-id\u003c/code\u003e, \u003ccode\u003egithub-token\u003c/code\u003e ve diğerleri — kendi sabit eşleşme kriterlerine sahiptir ve entropiden bağımsız olarak bu ayar tarafından \u003cstrong\u003ehiçbir zaman\u003c/strong\u003e bırakılmaz.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"dorulama\"\u003eDoğrulama\u003c/h2\u003e\n\u003cp\u003eLeakwatch, taramaya başlamadan önce yüklenen yapılandırmayı doğrular ve aşağıdaki durumların herhangi birinde hata vererek çıkar:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eKoşul\u003c/th\u003e\n\u003cth\u003eHata\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003escan.concurrency \u0026lt; 1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGeçersiz eşzamanlılık değeri\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003escan.max-file-size \u0026lt; 1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGeçersiz max-file-size değeri\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eoutput.format\u003c/code\u003e \u003ccode\u003ejson|sarif|csv|table|github\u003c/code\u003e içinde değil\u003c/td\u003e\n\u003ctd\u003eDesteklenmeyen çıktı biçimi\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edetection.entropy.threshold\u003c/code\u003e 0–8 dışında\u003c/td\u003e\n\u003ctd\u003eGeçersiz entropi eşiği\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eoutput.severity-threshold\u003c/code\u003e geçerli bir seviye değil (boş değilse)\u003c/td\u003e\n\u003ctd\u003eGeçersiz severity-threshold\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003everification.timeout \u0026lt; 1ms\u003c/code\u003e (doğrulama etkinleştirildiğinde)\u003c/td\u003e\n\u003ctd\u003eGeçersiz doğrulama zaman aşımı\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003everification.concurrency \u0026lt; 1\u003c/code\u003e (doğrulama etkinleştirildiğinde)\u003c/td\u003e\n\u003ctd\u003eGeçersiz doğrulama eşzamanlılığı\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003everification.rate-limit \u0026lt;= 0\u003c/code\u003e (doğrulama etkinleştirildiğinde)\u003c/td\u003e\n\u003ctd\u003eGeçersiz doğrulama rate-limit\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/ignoring-findings\"\u003eBulguları Yok Sayma\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/severity-and-filtering\"\u003eÖnem Derecesi \u0026amp; Filtreleme\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/detectors/custom-rules\"\u003eÖzel Kurallar\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/environment-variables\"\u003eOrtam Değişkenleri\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n"},"configuration/ignoring-findings":{"title":"Bulguları Yok Sayma","description":".leakwatchignore dosyaları, satır içi yok sayma işaretçileri ve yerleşik ikili dosya ve kilit dosyası atlamaları ile yanlış pozitifleri bastırın.","html":"\u003ch1 id=\"bulgular-yok-sayma\"\u003eBulguları Yok Sayma\u003c/h1\u003e\n\u003cp\u003eHiçbir tarayıcının yanlış pozitif oranı sıfır değildir. Leakwatch, gürültüyü bastırmak için size üç katmanlı mekanizma sunar: yol tabanlı dışlamalar için bir \u003ccode\u003e.leakwatchignore\u003c/code\u003e dosyası, satır düzeyinde bastırma için satır içi işaretçiler ve ikili dosyalar ile yaygın kilit dosyaları için her zaman etkin olan yerleşik atlamalar.\u003c/p\u003e\n\u003ch2 id=\"leakwatchignore-dosyas\"\u003e\u003ccode\u003e.leakwatchignore\u003c/code\u003e dosyası\u003c/h2\u003e\n\u003cp\u003eTarama sonuçlarından yolları hariç tutmak için depo kökünüze (veya geçerli dizine) bir \u003ccode\u003e.leakwatchignore\u003c/code\u003e dosyası oluşturun. Gitignore stilinde söz dizimi kullanır:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e#\u003c/code\u003e ile başlayan satırlar yorum satırlarıdır.\u003c/li\u003e\n\u003cli\u003eBoş satırlar atlanır.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e!\u003c/code\u003e öneki bir deseni \u003cstrong\u003egeçersiz kılar\u003c/strong\u003e; önceki bir desen tarafından dışlanmış olacak bir yolu yeniden dahil eder.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSon eşleşen desen kazanır\u003c/strong\u003e — sıra önemlidir.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"ykleme-sras\"\u003eYükleme sırası\u003c/h3\u003e\n\u003cp\u003eLeakwatch, bulduğu \u003cstrong\u003eilk\u003c/strong\u003e \u003ccode\u003e.leakwatchignore\u003c/code\u003e dosyasını yükler; önce tarama kökünü, ardından yedek olarak geçerli çalışma dizinini kontrol eder. Her tarama için yalnızca bir dosya kullanılır — tarama kökünde bir \u003ccode\u003e.leakwatchignore\u003c/code\u003e varsa, geçerli dizindeki dosya (varsa) hiçbir zaman açılmaz, birleştirilmesi bir yana. Dosyalar arası öncelik veya birleştirme yoktur: öngörülebilir bir davranış için kurallarınızı tarama kökündeki tek bir \u003ccode\u003e.leakwatchignore\u003c/code\u003e dosyasında toplayın.\u003c/p\u003e\n\u003ch3 id=\"glob-sz-dizimi\"\u003eGlob söz dizimi\u003c/h3\u003e\n\u003cp\u003eÜç desen stili desteklenir:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eStil\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003cth\u003eÖrnek\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003eStandart glob\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efilepath.Match\u003c/code\u003e stili, hem tam yola hem de temel dosya adına karşı eşleştirilen\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e*.pem\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eÇift yıldız \u003ccode\u003e**\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSıfır veya daha fazla yol segmentini kapsar\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003etest/fixtures/**\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eSondaki eğik çizgi \u003ccode\u003edir/\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAdlandırılmış dizinin herhangi bir derinliğindeki her dosyayla eşleşir\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003esnapshots/\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"leakwatchignore-rnei\"\u003e\u003ccode\u003e.leakwatchignore\u003c/code\u003e örneği\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003e# Tüm test fixture dosyalarını yok say\ntest/fixtures/**\n\n# Dokümantasyondaki bilinen yer tutucu anahtarları yok say\ndocs/examples/\n\n# Ağaçtaki herhangi bir yerdeki belirli uzantılı dosyaları yok say\n*.pem.example\n\n# Yukarıdaki kural tarafından dışlanan belirli bir dosyayı yeniden dahil et\n!docs/examples/real-config-sample.yaml\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNot\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003e\u003ccode\u003e.leakwatchignore\u003c/code\u003e filtrelemesi, her bulgunun dosya yoluna göre tarama tamamlandıktan \u003cstrong\u003esonra\u003c/strong\u003e uygulanır. Dosyaların okunmasını engellemez — ürettikleri bulguları bastırır. Dosyaları okunmadan önce atlamak için yapılandırma dosyasında \u003ccode\u003efilter.exclude-paths\u003c/code\u003e veya \u003ccode\u003escan fs\u003c/code\u003e komutunda \u003ccode\u003e--exclude\u003c/code\u003e kullanın.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"satr-ii-yok-sayma-iaretileri\"\u003eSatır içi yok sayma işaretçileri\u003c/h2\u003e\n\u003cp\u003eSöz konusu satırdaki dedektörleri bastırmak için herhangi bir kaynak satırına doğrudan bir işaretçi koyun. İşaretçi satırın herhangi bir yerine yerleştirilebilir — genellikle bir yorum içinde — ve motor tarafından doğrulamadan \u003cstrong\u003eönce\u003c/strong\u003e uygulanır; böylece yok sayılan bir satır hiçbir zaman ağ çağrısını tetiklemez.\u003c/p\u003e\n\u003ch3 id=\"bir-satrdaki-tm-dedektrleri-bastr\"\u003eBir satırdaki tüm dedektörleri bastır\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-python\"\u003e# Ödeme işleme yapılandırması\nSTRIPE_KEY = \u0026quot;sk_test_XXXXXXXXXXXXXXXXXXXX\u0026quot; # leakwatch:ignore\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"bir-satrdaki-belirli-bir-dedektr-bastr\"\u003eBir satırdaki belirli bir dedektörü bastır\u003c/h3\u003e\n\u003cp\u003eYalnızca bir dedektörü bastırırken diğerlerini etkin bırakmak için \u003ccode\u003eleakwatch:ignore:\u0026lt;detector-id\u0026gt;\u003c/code\u003e kullanın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-go\"\u003e// Bu token dokümantasyon için kasıtlı olarak bir yer tutucudur\nexampleToken := \u0026quot;ghp_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\u0026quot; // leakwatch:ignore:github-token\n\u003c/code\u003e\u003c/pre\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e# Platform tarafından ayarlanan CI ortam değişkeni — gerçek bir sır değil\napi_key: \u0026quot;${CI_API_KEY_PLACEHOLDER}\u0026quot; # leakwatch:ignore:generic-api-key\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eİpucu\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eMümkün olduğunda genel form yerine dedektöre özgü formu (\u003ccode\u003eleakwatch:ignore:\u0026lt;detector-id\u0026gt;\u003c/code\u003e) tercih edin. Hangi dedektörü bastırdığınızı belgeler ve diğer tüm dedektörleri o satırda etkin bırakır.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"yerleik-atlamalar-her-zaman-uygulanr\"\u003eYerleşik atlamalar (her zaman uygulanır)\u003c/h2\u003e\n\u003cp\u003eLeakwatch, herhangi bir dedektörü çalıştırmadan önce aşağıdakileri koşulsuz olarak atlar:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eİkili dosya uzantıları\u003c/strong\u003e — \u003ccode\u003e.exe\u003c/code\u003e, \u003ccode\u003e.dll\u003c/code\u003e, \u003ccode\u003e.so\u003c/code\u003e, \u003ccode\u003e.dylib\u003c/code\u003e, \u003ccode\u003e.bin\u003c/code\u003e, \u003ccode\u003e.png\u003c/code\u003e, \u003ccode\u003e.jpg\u003c/code\u003e, \u003ccode\u003e.gif\u003c/code\u003e, \u003ccode\u003e.mp4\u003c/code\u003e, \u003ccode\u003e.zip\u003c/code\u003e, \u003ccode\u003e.tar\u003c/code\u003e, \u003ccode\u003e.gz\u003c/code\u003e, \u003ccode\u003e.pdf\u003c/code\u003e, \u003ccode\u003e.woff\u003c/code\u003e, \u003ccode\u003e.ttf\u003c/code\u003e ve diğerleri gibi uzantılara sahip dosyalar hiçbir zaman taranmaz.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eİkili içerik tespiti\u003c/strong\u003e — ilk 8 KB'ı null bayt içeren herhangi bir dosya, uzantısından bağımsız olarak ikili olarak kabul edilir ve atlanır.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eYaygın kilit dosyaları\u003c/strong\u003e — aşağıdaki dosya adları, yüksek oranda yanlış pozitif üreten hash ve sağlama toplamları içerdikleri için her zaman atlanır:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eDosya\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epackage-lock.json\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eyarn.lock\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epnpm-lock.yaml\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecomposer.lock\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eGemfile.lock\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eCargo.lock\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epoetry.lock\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ego.sum\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ePipfile.lock\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eBu yerleşik atlamalar devre dışı bırakılamaz. \u003ccode\u003efilter.exclude-paths\u003c/code\u003e ayarından ayrıdır ve yapılandırma tabanlı filtrelemeden önce çalışır.\u003c/p\u003e\n\u003ch2 id=\"tarama-ncesi-yol-tabanl-dlama\"\u003eTarama öncesi yol tabanlı dışlama\u003c/h2\u003e\n\u003cp\u003eYolları tarama motoru tarafından okunmadan önce dışlamak için yapılandırma dosyanızda \u003ccode\u003efilter.exclude-paths\u003c/code\u003e kullanın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003efilter:\n exclude-paths:\n - \u0026quot;vendor/**\u0026quot;\n - \u0026quot;node_modules/**\u0026quot;\n - \u0026quot;**/*.min.js\u0026quot;\n - \u0026quot;third-party/\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBu ayar \u003cstrong\u003etüm tarama kaynaklarına\u003c/strong\u003e uygulanır (dosya sistemi, Git geçmişi, konteyner imajları, bulut depolama, Slack). Yolları tarayan her \u003ccode\u003escan\u003c/code\u003e alt komutunda (\u003ccode\u003efs\u003c/code\u003e, \u003ccode\u003egit\u003c/code\u003e, \u003ccode\u003eimage\u003c/code\u003e, \u003ccode\u003es3\u003c/code\u003e, \u003ccode\u003egcs\u003c/code\u003e, \u003ccode\u003erepos\u003c/code\u003e — bunun yerine \u003ccode\u003e--exclude-channels\u003c/code\u003e ile dışlama yapan \u003ccode\u003eslack\u003c/code\u003e hariç) komut satırında da \u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e geçirebilirsiniz; tekrarlanabilir olan bu bayrak, \u003ccode\u003efilter.exclude-paths\u003c/code\u003e'in yerine geçmek yerine onunla birleştirilir.\u003c/p\u003e\n\u003cp\u003eYapılandırma dosyasını düzenlemeden gürültülü bir dedektörü tek bir çalıştırma için susturmak üzere, herhangi bir tarama alt komutunda \u003ccode\u003e--exclude-detectors \u0026lt;id\u0026gt;[,\u0026lt;id\u0026gt;...]\u003c/code\u003e (tekrarlanabilir) geçirin — bkz. \u003ca href=\"#/detectors/detector-catalog\"\u003eDedektör Kataloğu\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eTam yapılandırma şeması için \u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e, dedektör düzeyinde ve önem derecesi düzeyinde filtreleme için \u003ca href=\"#/configuration/severity-and-filtering\"\u003eÖnem Derecesi \u0026amp; Filtreleme\u003c/a\u003e bölümlerine bakın.\u003c/p\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/severity-and-filtering\"\u003eÖnem Derecesi \u0026amp; Filtreleme\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n"},"configuration/severity-and-filtering":{"title":"Önem Derecesi \u0026 Filtreleme","description":"Önem eşikleri, yalnızca doğrulanmış mod, dedektör dışlamaları ve yol dışlamaları kullanarak hangi bulguların çıktınıza ulaşacağını kontrol edin.","html":"\u003ch1 id=\"nem-derecesi--filtreleme\"\u003eÖnem Derecesi \u0026amp; Filtreleme\u003c/h1\u003e\n\u003cp\u003eYoğun bir kod tabanı çok sayıda bulgu üretebilir. Leakwatch, en önemli sinyallere odaklanmak için birleştirebileceğiniz birkaç bağımsız filtre sunar: önem eşikleri düşük öncelikli gürültüyü eler, yalnızca doğrulanmış mod yalnızca onaylanmış canlı sırları ortaya çıkarır, dedektör dışlamaları bilinen yanlış pozitif kaynakları susturur ve yol dışlamaları tüm dizin ağaçlarını kapsamın dışında bırakır.\u003c/p\u003e\n\u003ch2 id=\"nem-seviyeleri\"\u003eÖnem seviyeleri\u003c/h2\u003e\n\u003cp\u003eHer yerleşik dedektör, varsayılan bir önem derecesiyle birlikte gelir. En düşükten en yüksek önceliğe doğru dört seviye şunlardır:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eSeviye\u003c/th\u003e\n\u003cth\u003eTipik kullanım\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDaha yüksek yanlış pozitif oranına sahip genel desenler\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003emedium\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTanınabilir kimlik bilgisi biçimleri, doğrulanmamış\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ehigh\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMaruziyetin büyük olasılıkla önemli olduğu iyi yapılandırılmış sırlar\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecritical\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOnaylanmış canlı sırlar veya neredeyse sıfır yanlış pozitif oranlı biçimler\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eHer dedektöre atanan önem derecesi \u003ca href=\"#/detectors/detector-catalog\"\u003eDedektör Kataloğu\u003c/a\u003e'nda listelenmiştir.\u003c/p\u003e\n\u003ch2 id=\"--min-severity-eiin-altndaki-bulgular-brak\"\u003e\u003ccode\u003e--min-severity\u003c/code\u003e: eşiğin altındaki bulguları bırak\u003c/h2\u003e\n\u003cp\u003eBelirtilen seviyenin altındaki önem derecesine sahip bulguları atmak için \u003ccode\u003e--min-severity \u0026lt;level\u0026gt;\u003c/code\u003e parametresini kullanın. Yalnızca eşik değerinde veya üzerindeki bulgular çıktıya ulaşır.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Yalnızca high ve critical bulguları göster\nleakwatch scan fs . --min-severity high\n\n# medium, high ve critical bulguları göster\nleakwatch scan fs . --min-severity medium\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003eoutput.severity-threshold\u003c/code\u003e altında yapılandırma dosyasında kalıcı bir varsayılan ayarlayabilirsiniz. \u003ccode\u003e--min-severity\u003c/code\u003e bayrağı, çalışma zamanında yapılandırma değerini geçersiz kılar:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003eoutput:\n severity-threshold: medium\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"--only-verified-yalnzca-onaylanm-aktif-srlar\"\u003e\u003ccode\u003e--only-verified\u003c/code\u003e: yalnızca onaylanmış aktif sırlar\u003c/h2\u003e\n\u003cp\u003eYalnızca doğrulama durumu \u003ccode\u003everified_active\u003c/code\u003e olan bulguları, yani Leakwatch'ın sağlayıcı API'sine kontrollü bir salt-okunur çağrı yaparak hâlâ geçerli olduğunu doğruladığı sırları tutmak için \u003ccode\u003e--only-verified\u003c/code\u003e parametresini kullanın. Diğer tüm bulgular (\u003ccode\u003eunverified\u003c/code\u003e, \u003ccode\u003everified_inactive\u003c/code\u003e veya \u003ccode\u003everify_error\u003c/code\u003e) bırakılır.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --only-verified\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBu bayrak, derlemeyi yalnızca onaylanmış olaylar üzerinde, yer tutucu veya zaten döndürülmüş kimlik bilgileri olabilecek şüpheli desenler üzerinde değil, başarısız kılmak istediğiniz CI hatlarında en kullanışlıdır.\u003c/p\u003e\n\u003cp\u003eHangi dedektörlerin canlı doğrulamayı desteklediği için \u003ca href=\"#/verification/how-verification-works\"\u003eDoğrulama Nasıl Çalışır\u003c/a\u003e bölümüne bakın.\u003c/p\u003e\n\u003ch2 id=\"filterexclude-detectors-belirli-dedektrleri-devre-d-brak\"\u003e\u003ccode\u003efilter.exclude-detectors\u003c/code\u003e: belirli dedektörleri devre dışı bırak\u003c/h2\u003e\n\u003cp\u003eBir veya daha fazla dedektörü kalıcı olarak devre dışı bırakmak için ID'lerini yapılandırma dosyasındaki \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e altında listeleyin. Listelenen dedektörlerden gelen bulgular, diğer ayarlardan bağımsız olarak hiçbir zaman üretilmez:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003efilter:\n exclude-detectors:\n - generic-api-key\n - jwt\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eDedektör ID'leri \u003ca href=\"#/detectors/detector-catalog\"\u003eDedektör Kataloğu\u003c/a\u003e'nda listelenmiştir. Bir dedektör sürekli olarak kod tabanınız için yanlış pozitifler ürettiğinde ve diğer bastırma mekanizmaları (satır içi yok saymalar veya \u003ccode\u003e.leakwatchignore\u003c/code\u003e) yeterince ayrıntılı olmadığında bu ayarı kullanın.\u003c/p\u003e\n\u003cp\u003eYapılandırma dosyasını düzenlemeden bir dedektörü tek bir çalıştırma için susturmak üzere, herhangi bir tarama alt komutunda \u003ccode\u003e--exclude-detectors \u0026lt;id\u0026gt;[,\u0026lt;id\u0026gt;...]\u003c/code\u003e (tekrarlanabilir) geçirin. Bu, \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e'ın yerine geçmek yerine ona ekleme yapar.\u003c/p\u003e\n\u003ch2 id=\"filterexclude-paths-tarama-ncesi-yollar-atla\"\u003e\u003ccode\u003efilter.exclude-paths\u003c/code\u003e: tarama öncesi yolları atla\u003c/h2\u003e\n\u003cp\u003eYolları tarama motoru okumadan önce dışlamak için yapılandırma dosyasında \u003ccode\u003efilter.exclude-paths\u003c/code\u003e kullanın. Desenler, \u003ccode\u003e.leakwatchignore\u003c/code\u003e ile aynı glob söz dizimini kullanır (standart globlar, \u003ccode\u003e**\u003c/code\u003e çift yıldız ve sondaki eğik çizgili dizin desenleri) ve \u003cstrong\u003etüm tarama kaynaklarına\u003c/strong\u003e uygulanır:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003efilter:\n exclude-paths:\n - \u0026quot;vendor/**\u0026quot;\n - \u0026quot;node_modules/**\u0026quot;\n - \u0026quot;**/*.min.js\u0026quot;\n - \u0026quot;**/*.min.css\u0026quot;\n - \u0026quot;test/fixtures/\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNot\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003e\u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e bayrağı, \u003ccode\u003efilter.exclude-paths\u003c/code\u003e'in komut satırı eşdeğeridir ve \u003ccode\u003escan slack\u003c/code\u003e \u003cstrong\u003ehariç\u003c/strong\u003e (bunun yerine \u003ccode\u003e--exclude-channels\u003c/code\u003e ile dışlama yapan) her tarama alt komutunda kullanılabilir. Tekrarlanabilir olup \u003ccode\u003efilter.exclude-paths\u003c/code\u003e'in yerine geçmek yerine ona ekleme yapar.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"cida-filtreleri-birletirme\"\u003eCI'da filtreleri birleştirme\u003c/h2\u003e\n\u003cp\u003eBir CI hattında genellikle yalnızca gerçek olaylarda başarısız olan, düşük gürültülü ve yüksek sinyalli bir çalışma istersiniz. Önerilen bir kombinasyon:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . \\\n --only-verified \\\n --min-severity high \\\n --format sarif \\\n --output results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eYapılandırma dosyasının kalıcı yol dışlamalarını yönetmesiyle:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003efilter:\n exclude-paths:\n - \u0026quot;vendor/**\u0026quot;\n - \u0026quot;node_modules/**\u0026quot;\n - \u0026quot;test/fixtures/\u0026quot;\n exclude-detectors:\n - generic-api-key\n\noutput:\n severity-threshold: high\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eArdından CI için yalnızca biçimi ve hedefi komut satırında geçersiz kılın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --only-verified --format sarif --output results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eDoğrulama ayrıntıları için \u003ca href=\"#/verification/how-verification-works\"\u003eDoğrulama Nasıl Çalışır\u003c/a\u003e, satır içi ve dosya tabanlı bastırma için \u003ca href=\"#/configuration/ignoring-findings\"\u003eBulguları Yok Sayma\u003c/a\u003e ve tam şema için \u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e bölümlerine bakın.\u003c/p\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/detectors/detector-catalog\"\u003eDedektör Kataloğu\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eDoğrulama Nasıl Çalışır\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/ignoring-findings\"\u003eBulguları Yok Sayma\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n"},"detectors/custom-rules":{"title":"Özel Kurallar","description":"YAML ile kendi sır tespit kalıplarınızı nasıl tanımlayacağınız ve 64 yerleşik dedektörün yanında bir Leakwatch taramasına nasıl ekleyeceğiniz.","html":"\u003ch1 id=\"zel-kurallar\"\u003eÖzel Kurallar\u003c/h1\u003e\n\u003cp\u003e64 yerleşik dedektör yaygın kullanılan kimlik bilgisi formatlarını kapsar; ancak her kuruluşun dahili token'ları, özel servis anahtarları veya hiçbir genel aracın önceden tahmin edemeyeceği ortama özgü kalıpları vardır. Özel kurallar, kaynak kodu değiştirmeden veya ikili dosyayı yeniden derlemeden kendi kalıplarınızı düz YAML ile tanımlamanıza ve çalışma zamanında yüklemenize olanak tanıyarak Leakwatch'ı genişletmenizi sağlar.\u003c/p\u003e\n\u003ch2 id=\"zel-kurallar-nerede-tanmlanr\"\u003eÖzel kurallar nerede tanımlanır\u003c/h2\u003e\n\u003cp\u003eÖzel kurallar, \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e yapılandırma dosyanızda en üst düzey bir \u003ccode\u003ecustom-rules:\u003c/code\u003e listesi altında tanımlanır:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003ecustom-rules:\n - id: acme-internal-token\n description: \u0026quot;ACME Corp dahili servis token'ı\u0026quot;\n regex: 'acme_[a-z0-9]{32}'\n keywords:\n - acme_\n severity: critical\n entropy: 3.5\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eKurallar, Leakwatch başladığında çalışma zamanında kaydedilir. Aynı Aho-Corasick ön-filtre hattını kullanarak yerleşik dedektörlerle birlikte çalışırlar.\u003c/p\u003e\n\u003ch2 id=\"kural-alanlar\"\u003eKural alanları\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eAlan\u003c/th\u003e\n\u003cth\u003eZorunlu\u003c/th\u003e\n\u003cth\u003eTür\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eid\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eEvet\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003eBenzersiz dedektör ID'si. Çıktıda ve \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e içinde kullanılır. Yerleşik dedektör ID'si veya başka bir özel kural ID'si ile çakışmamalıdır.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edescription\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHayır\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003eÇıktıda gösterilen insan tarafından okunabilir açıklama.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eregex\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eEvet\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003eRE2 uyumlu düzenli ifade. Maksimum 4096 karakter.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ekeywords\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHayır\u003c/td\u003e\n\u003ctd\u003estring listesi\u003c/td\u003e\n\u003ctd\u003eAho-Corasick ön-filtre anahtar kelimeleri. Regex yalnızca bu dizelerden en az birini içeren parçalar üzerinde çalışır. Bu alanın atlanması regex'in her parça üzerinde çalışmasına neden olur.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eseverity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHayır\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ecritical\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e veya \u003ccode\u003elow\u003c/code\u003e. Varsayılan \u003ccode\u003emedium\u003c/code\u003e'dur.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eentropy\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHayır\u003c/td\u003e\n\u003ctd\u003efloat\u003c/td\u003e\n\u003ctd\u003eShannon entropi eşiği (0–8). Entropisi bu değerin \u003cstrong\u003ealtında\u003c/strong\u003e olan eşleşmeler atılır. Düşük rastgelelikli yanlış pozitifleri filtrelemek için kullanışlıdır.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eİpucu\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eHer zaman \u003ccode\u003ekeywords\u003c/code\u003e belirtin. Tek kısa bir anahtar kelime bile (token ön eki gibi) regex motorunun işlediği parça sayısını önemli ölçüde azaltır ve büyük depolarda taramaların hızlı kalmasını sağlar. Örneğin tüm dahili token'larınız \u003ccode\u003eacme_\u003c/code\u003e ile başlıyorsa \u003ccode\u003ekeywords: [acme_]\u003c/code\u003e ayarlayın.\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eentropy\u003c/code\u003e kullanarak \u003ccode\u003eacme_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u003c/code\u003e gibi kalıbı karşılayan ancak açıkça gerçek sır olmayan yer tutucu değerlerdeki eşleşmeleri bastırın. 3,0–3,5 civarı bir eşik iyi bir başlangıç noktasıdır.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"akma-ynetimi\"\u003eÇakışma yönetimi\u003c/h2\u003e\n\u003cp\u003eBir özel kuralın \u003ccode\u003eid\u003c/code\u003e'si zaten kayıtlı bir dedektörle eşleşirse — yerleşik dedektör veya daha önce yüklenen özel kural olsun fark etmez — yinelenen kural \u003cstrong\u003eatlanır\u003c/strong\u003e ve bir hata loglanır. Leakwatch çökmez; geri kalan kurallar normal şekilde yüklenir. Bir özel kuralın etkisiz göründüğü durumlarda log çıktısını kontrol edin.\u003c/p\u003e\n\u003ch2 id=\"dorulama\"\u003eDoğrulama\u003c/h2\u003e\n\u003cp\u003eÖzel kuralların eşleştirilmiş doğrulayıcısı yoktur. Özel kurallardan gelen bulgular her zaman \u003ccode\u003eunverified\u003c/code\u003e durumuyla raporlanır — hiçbir zaman \u003ccode\u003everified_active\u003c/code\u003e veya \u003ccode\u003everified_inactive\u003c/code\u003e olmaz.\u003c/p\u003e\n\u003ch2 id=\"tam-rnek\"\u003eTam örnek\u003c/h2\u003e\n\u003cp\u003eAşağıdaki \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e, iki özel kural tanımlar: biri dahili servis token'ı, diğeri webhook'larda kullanılan imzalama sırrı için.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003ecustom-rules:\n - id: acme-internal-token\n description: \u0026quot;ACME Corp dahili servis token'ı (format: acme_ + 32 hex karakter)\u0026quot;\n regex: 'acme_[a-f0-9]{32}'\n keywords:\n - acme_\n severity: critical\n entropy: 3.2\n\n - id: acme-webhook-signing-secret\n description: \u0026quot;ACME Corp webhook imzalama sırrı (format: whsec_ + 40 base64url karakter)\u0026quot;\n regex: 'whsec_[A-Za-z0-9_\\-]{40}'\n keywords:\n - whsec_\n severity: high\n entropy: 3.5\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBu yapılandırmayla bir tarama çalıştırın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --config .leakwatch.yaml\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eÖzel kural bulgusu için örnek JSON çıktısı (sır değeri maskelenmiştir) — bir özel kural bulgusu, her yerleşik dedektörle tamamen aynı \u003ccode\u003eFinding\u003c/code\u003e şemasını kullanır (bkz. \u003ca href=\"#/output/output-formats\"\u003eÇıktı Biçimleri\u003c/a\u003e); özel kurallar için ayrı bir şema yoktur:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-json\"\u003e{\n \u0026quot;id\u0026quot;: \u0026quot;8a403a46336fc6ffdc847d202b74536c\u0026quot;,\n \u0026quot;detector_id\u0026quot;: \u0026quot;acme-internal-token\u0026quot;,\n \u0026quot;severity\u0026quot;: \u0026quot;critical\u0026quot;,\n \u0026quot;redacted\u0026quot;: \u0026quot;****cdef\u0026quot;,\n \u0026quot;source\u0026quot;: {\n \u0026quot;source_type\u0026quot;: \u0026quot;filesystem\u0026quot;,\n \u0026quot;file_path\u0026quot;: \u0026quot;config/production.env\u0026quot;,\n \u0026quot;line\u0026quot;: 14\n },\n \u0026quot;verification\u0026quot;: {\n \u0026quot;status\u0026quot;: \u0026quot;unverified\u0026quot;\n },\n \u0026quot;detected_at\u0026quot;: \u0026quot;2026-05-23T10:15:30Z\u0026quot;,\n \u0026quot;entropy\u0026quot;: 4.12\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNot\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003e\u003ccode\u003eredacted\u003c/code\u003e alanı gerçek sırrı her zaman maskeler (varsayılan olarak, \u003ccode\u003e****\u003c/code\u003e ve ardından en fazla son 4 karakter). Ham değer, açıkça \u003ccode\u003e--show-raw\u003c/code\u003e geçilmedikçe çıktıya asla yazılmaz (kontrollü ortamlar dışında önerilmez). Özel kuralların doğrulayıcısı olmadığından, \u003ccode\u003everification.status\u003c/code\u003e her zaman \u003ccode\u003eunverified\u003c/code\u003e olur.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"zel-kural-hari-tutma\"\u003eÖzel kuralı hariç tutma\u003c/h2\u003e\n\u003cp\u003eÖzel kurallar, yerleşik dedektörlerle aynı filtrelemeye katılır. Bir özel kuralı yapılandırmadan kaldırmadan devre dışı bırakmak için:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003efilter:\n exclude-detectors:\n - acme-internal-token\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eYapılandırma: Yapılandırma Dosyası\u003c/a\u003e — \u003ccode\u003ecustom-rules:\u003c/code\u003e öğesinin belge yapısındaki yeri dahil \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e için tam referans.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/detectors/detector-catalog\"\u003eDedektör Kataloğu\u003c/a\u003e — özel kuralınızı adlandırmadan önce ID çakışmalarını kontrol etmek için 64 yerleşik dedektör.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/how-it-works\"\u003eNasıl Çalışır\u003c/a\u003e — \u003ccode\u003ekeywords\u003c/code\u003e öğesinin bağlandığı Aho-Corasick ön-filtre hattı.\u003c/li\u003e\n\u003c/ul\u003e\n"},"detectors/detector-catalog":{"title":"Dedektör Kataloğu","description":"Kategorilere göre gruplanmış tüm 64 yerleşik dedektör; ID'leri, ne tespit ettikleri ve varsayılan önem dereceleri ile.","html":"\u003ch1 id=\"dedektr-katalou\"\u003eDedektör Kataloğu\u003c/h1\u003e\n\u003cp\u003eLeakwatch, bulut sağlayıcısı erişim anahtarlarından ve yapay zekâ API token'larından veritabanı bağlantı dizelerine ve özel kriptografik anahtarlara kadar geniş bir kimlik bilgisi türü yelpazesini kapsayan \u003cstrong\u003e64 yerleşik dedektör\u003c/strong\u003e ile gelir. Her dedektörün kararlı bir ID'si, varsayılan bir önem derecesi ve (çoğu için) bulunan sırrın hâlâ canlı olup olmadığını teyit edebilen eşleştirilmiş bir doğrulayıcısı vardır.\u003c/p\u003e\n\u003cp\u003eBu sayfa her yerleşik dedektörü listeler. Doğrulama kapsamı ayrıntıları için \u003ca href=\"#/verification/verification-coverage\"\u003eDoğrulama Kapsamı\u003c/a\u003e bölümüne bakın. Kendi kalıplarınızı eklemek için \u003ca href=\"#/detectors/custom-rules\"\u003eÖzel Kurallar\u003c/a\u003e bölümüne bakın.\u003c/p\u003e\n\u003ch2 id=\"bu-katalogu-nasl-okuyacaksnz\"\u003eBu katalogu nasıl okuyacaksınız\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eID\u003c/strong\u003e — yapılandırma ve çıktıda kullanılan kararlı dize tanımlayıcısı. Bir dedektörü atlamak için \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e listesine ekleyin veya \u003ccode\u003e--min-severity\u003c/code\u003e filtrelemesiyle birlikte kullanın (\u003ca href=\"#/configuration/severity-and-filtering\"\u003eÖnem Derecesi \u0026amp; Filtreleme\u003c/a\u003e).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTespit eder\u003c/strong\u003e — dedektörün ne aradığı.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eÖnem Derecesi\u003c/strong\u003e — \u003ccode\u003eCritical\u003c/code\u003e (Kritik), \u003ccode\u003eHigh\u003c/code\u003e (Yüksek) veya \u003ccode\u003eMedium\u003c/code\u003e (Orta). Bu varsayılandır; \u003ccode\u003e--min-severity\u003c/code\u003e bayrağını ve \u003ccode\u003eoutput.severity-threshold\u003c/code\u003e yapılandırma anahtarını besler.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"bulut-ve-altyap\"\u003eBulut ve Altyapı\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eTespit eder\u003c/th\u003e\n\u003cth\u003eÖnem Derecesi\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eaws-access-key-id\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAWS Access Key ID\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egcp-service-account\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGCP Servis Hesabı Anahtarı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eazure-storage-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAzure Storage Bağlantı Dizesi\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eazure-entra-secret\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAzure Entra ID İstemci Sırrı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edigitalocean-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDigitalOcean Kişisel Erişim Token'ı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecloudflare-api-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCloudflare API Token'ı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eheroku-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHeroku API Anahtarı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003evercel-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eVercel API Token'ı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eterraform-cloud-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTerraform Cloud/Enterprise API Token'ı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ehashicorp-vault-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHashiCorp Vault Token'ı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edoppler-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDoppler Token — servis (\u003ccode\u003edp.st.\u003c/code\u003e), kişisel (\u003ccode\u003edp.pt.\u003c/code\u003e), CLI/yapılandırma (\u003ccode\u003edp.ct.\u003c/code\u003e) ve SCIM (\u003ccode\u003edp.scim.\u003c/code\u003e) token'ları\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"yapay-zek--makine-renimi\"\u003eYapay Zekâ / Makine Öğrenimi\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eTespit eder\u003c/th\u003e\n\u003cth\u003eÖnem Derecesi\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eopenai-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOpenAI API Anahtarı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eanthropic-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAnthropic API Anahtarı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edeepseek-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDeepSeek API Anahtarı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ehuggingface-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHugging Face API Token'ı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"demeler-ve-ticaret\"\u003eÖdemeler ve Ticaret\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eTespit eder\u003c/th\u003e\n\u003cth\u003eÖnem Derecesi\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003estripe-api-key-live\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eStripe Canlı API Anahtarı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003estripe-api-key-test\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eStripe Test API Anahtarı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecoinbase-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCoinbase API Anahtarı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eshopify-access-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eShopify Erişim Token'ı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"gelitirme-aralar-ci-ve-paketler\"\u003eGeliştirme Araçları, CI ve Paketler\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eTespit eder\u003c/th\u003e\n\u003cth\u003eÖnem Derecesi\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egithub-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGitHub Kişisel Erişim Token'ı — hem klasik \u003ccode\u003eghp_\u003c/code\u003e PAT hem de ince taneli \u003ccode\u003egithub_pat_\u003c/code\u003e PAT\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egithub-oauth-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGitHub OAuth2 ve kurulum (installation) token'ı — \u003ccode\u003egho_\u003c/code\u003e/\u003ccode\u003eghu_\u003c/code\u003e/\u003ccode\u003eghr_\u003c/code\u003e/\u003ccode\u003eghs_\u003c/code\u003e, yeni durumsuz (JWT biçimli) \u003ccode\u003eghs_\u003c/code\u003e kurulum token'ları dâhil\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egitlab-pat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGitLab Kişisel Erişim Token'ı — klasik \u003ccode\u003eglpat-\u003c/code\u003e token'ı artı yeni yönlendirilebilir önekler: dağıtım (\u003ccode\u003egldt-\u003c/code\u003e), runner (\u003ccode\u003eglrt-\u003c/code\u003e), CI/CD derleme ve tetikleyici (\u003ccode\u003eglcbt-\u003c/code\u003e/\u003ccode\u003eglptt-\u003c/code\u003e), OAuth uygulama sırrı (\u003ccode\u003egloas-\u003c/code\u003e) ve besleme (\u003ccode\u003eglft-\u003c/code\u003e) token'ları; yakınında bulunduğunda kendi barındırılan (self-hosted) GitLab sunucuları doğrulama için tespit edilir\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ebitbucket-app-password\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBitbucket Uygulama Parolası\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecircleci-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCircleCI Kişisel API Token'ı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003enpm-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNPM Erişim Token'ı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epypi-api-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePyPI API Token'ı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003erubygems-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRubyGems API Anahtarı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edockerhub-pat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDocker Hub Kişisel Erişim Token'ı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esonarcloud-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSonarCloud/SonarQube Token'ı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esnyk-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSnyk API Anahtarı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edatabricks-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDatabricks Kişisel Erişim Token'ı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003elaunchdarkly-sdk-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eLaunchDarkly SDK Anahtarı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"letiim-ve-birlii\"\u003eİletişim ve İşbirliği\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eTespit eder\u003c/th\u003e\n\u003cth\u003eÖnem Derecesi\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eslack-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSlack Bot/Kullanıcı Token'ı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eslack-webhook\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSlack Webhook URL'si\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eteams-webhook\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMicrosoft Teams Gelen Webhook URL'si\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ediscord-bot-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDiscord Bot Token'ı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ediscord-webhook-url\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDiscord Webhook URL'si\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003etelegram-bot-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTelegram Bot Token'ı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003enotion-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNotion Dahili Entegrasyon Token'ı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003elinear-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eLinear API Anahtarı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003efigma-pat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFigma Kişisel Erişim Token'ı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eairtable-pat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAirtable Kişisel Erişim Token'ı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"e-posta-ve-mesajlama-teslimat\"\u003eE-posta ve Mesajlaşma Teslimatı\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eTespit eder\u003c/th\u003e\n\u003cth\u003eÖnem Derecesi\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esendgrid-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSendGrid API Anahtarı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003emailgun-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMailgun API Anahtarı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epostmark-server-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePostmark Sunucu API Token'ı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003etwilio-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTwilio API Anahtarı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"zleme-ve-gzlemlenebilirlik\"\u003eİzleme ve Gözlemlenebilirlik\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eTespit eder\u003c/th\u003e\n\u003cth\u003eÖnem Derecesi\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edatadog-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDatadog API Anahtarı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003enewrelic-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNew Relic API Anahtarı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egrafana-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGrafana API Anahtarı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esentry-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSentry Kimlik Doğrulama Token'ı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epagerduty-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePagerDuty API Anahtarı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"veritabanlar-ve-balant-dizeleri\"\u003eVeritabanları ve Bağlantı Dizeleri\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eTespit eder\u003c/th\u003e\n\u003cth\u003eÖnem Derecesi\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edatabase-connection-string\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eVeritabanı Bağlantı Dizesi\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eredis-connection-string\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRedis Bağlantı Dizesi\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003erabbitmq-connection-string\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRabbitMQ Bağlantı Dizesi\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esnowflake-credentials\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSnowflake Bağlantı Kimlik Bilgileri\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esupabase-service-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSupabase Servis Rolü Anahtarı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"kimlik-ve-eriim\"\u003eKimlik ve Erişim\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eTespit eder\u003c/th\u003e\n\u003cth\u003eÖnem Derecesi\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eauth0-management-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAuth0 Yönetim API Token'ı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eokta-api-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOkta API Token'ı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eldap-credentials\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eLDAP/LDAPS Bağlama Kimlik Bilgileri\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"web3\"\u003eWeb3\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eTespit eder\u003c/th\u003e\n\u003cth\u003eÖnem Derecesi\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003einfura-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInfura API Anahtarı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"genel-ve-kriptografik\"\u003eGenel ve Kriptografik\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eTespit eder\u003c/th\u003e\n\u003cth\u003eÖnem Derecesi\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egeneric-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGenel API Anahtarı\u003c/td\u003e\n\u003ctd\u003eMedium\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ejwt\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eJSON Web Token\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eprivate-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÖzel Anahtar (RSA, SSH, DSA, EC, PGP); PKCS8 \u003ccode\u003ePRIVATE KEY\u003c/code\u003e ve parola korumalı \u003ccode\u003eENCRYPTED PRIVATE KEY\u003c/code\u003e zarfı dahil\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eftp-credentials\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFTP/SFTP Kimlik Bilgileri\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003chr\u003e\n\u003cp\u003e\u003cstrong\u003eToplam: 64 yerleşik dedektör.\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"nem-derecesine-gre-filtreleme\"\u003eÖnem derecesine göre filtreleme\u003c/h2\u003e\n\u003cp\u003eBulgular, komut satırında \u003ccode\u003e--min-severity\u003c/code\u003e veya yapılandırmada \u003ccode\u003eoutput.severity-threshold\u003c/code\u003e kullanılarak önem derecesine göre filtrelenebilir. Yalnızca belirtilen seviyede veya üzerindeki bulgular çıktıya dahil edilir. Ayrıntılar için \u003ca href=\"#/configuration/severity-and-filtering\"\u003eÖnem Derecesi \u0026amp; Filtreleme\u003c/a\u003e bölümüne bakın.\u003c/p\u003e\n\u003ch2 id=\"belirli-dedektrleri-hari-tutma\"\u003eBelirli dedektörleri hariç tutma\u003c/h2\u003e\n\u003cp\u003eBir veya daha fazla dedektörü tamamen atlamak için ID'lerini \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e içindeki \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e listesine ekleyin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003efilter:\n exclude-detectors:\n - generic-api-key\n - jwt\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eTam filtreleme referansı için \u003ca href=\"#/configuration/severity-and-filtering\"\u003eÖnem Derecesi \u0026amp; Filtreleme\u003c/a\u003e bölümüne bakın.\u003c/p\u003e\n\u003ch2 id=\"dorulama-kapsam\"\u003eDoğrulama kapsamı\u003c/h2\u003e\n\u003cp\u003eBazı dedektörlerin canlı doğrulayıcısı vardır; bazıları yalnızca format doğrulamasına tabi tutulur; onu ise hiç doğrulayıcıya sahip değildir. Tam döküm için \u003ca href=\"#/verification/verification-coverage\"\u003eDoğrulama Kapsamı\u003c/a\u003e bölümüne bakın.\u003c/p\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/detectors/custom-rules\"\u003eÖzel Kurallar\u003c/a\u003e — YAML ile kendi tespit kalıplarınızı tanımlayın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/verification-coverage\"\u003eDoğrulama Kapsamı\u003c/a\u003e — hangi dedektörlerin canlı doğrulanabileceği.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/severity-and-filtering\"\u003eÖnem Derecesi \u0026amp; Filtreleme\u003c/a\u003e — bulguları önem derecesine veya dedektöre göre filtreleme.\u003c/li\u003e\n\u003c/ul\u003e\n"},"getting-started/how-it-works":{"title":"Nasıl Çalışır","description":"Leakwatch tarama hattının mimarisi: kaynaklar, tespit, doğrulama ve çıktı.","html":"\u003ch1 id=\"nasl-alr\"\u003eNasıl Çalışır\u003c/h1\u003e\n\u003cp\u003eLeakwatch hattını anlamak, performansı ayarlamanıza, sonuçları yorumlamanıza ve hangi bayrakları kullanacağınıza karar vermenize yardımcı olur. Bu sayfa, bir tarama komutunu çalıştırdığınız andan bir bulgunun çıktınızda göründüğü ana kadar neler olduğunu açıklar.\u003c/p\u003e\n\u003ch2 id=\"hatta-genel-bak\"\u003eHatta genel bakış\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-mermaid\"\u003eflowchart LR\n A([Kaynak\\nfs / git / image\\ns3 / gcs / slack]) --\u0026gt; B[İşçi Havuzu\\n—concurrency işçi]\n B --\u0026gt; C[Aho-Corasick\\nÖn-Filtre]\n C --\u0026gt; D[Regex\\nDedektörler]\n D --\u0026gt; E[Satır İçi İgnore\\nKontrolü]\n E --\u0026gt; F[Doğrulama\\nHavuzu\\n4 işçi / 10 rps]\n F --\u0026gt; G[Tarama Sonrası\\nFiltreler]\n G --\u0026gt; H([Çıktı\\njson / sarif\\ncsv / table\\ngithub])\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eHer aşama aşağıda ayrıntılı olarak açıklanmaktadır.\u003c/p\u003e\n\u003ch2 id=\"1-kaynak\"\u003e1. Kaynak\u003c/h2\u003e\n\u003cp\u003eHer tarama, motorun işlemesi için veri parçaları yayan bir soyutlama olan \u003cstrong\u003eKaynak\u003c/strong\u003e ile başlar. Leakwatch altı kaynak ile birlikte gelir:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eKaynak\u003c/th\u003e\n\u003cth\u003eKomut\u003c/th\u003e\n\u003cth\u003eNe yayar\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003eDosya sistemi\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003escan fs\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYerel bir dizin ağacındaki dosya içerikleri\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eGit geçmişi\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003escan git\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTüm commit geçmişindeki her blob\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eKonteyner imajı\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003escan image\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBir OCI/Docker imajının katman içerikleri, daemonsuz\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eAWS S3\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003escan s3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBir S3 kovasındaki nesne içerikleri\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eGoogle Cloud Storage\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003escan gcs\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBir GCS kovasındaki nesne içerikleri\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eSlack\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003escan slack\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eKanal ve DM'lerdeki mesaj metni\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNot\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eSlack taraması yalnızca \u003cstrong\u003emesaj metnini\u003c/strong\u003e kapsar. Slack'e yüklenen dosyaların içerikleri taranmaz.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003cp\u003eParçalar, işçi havuzu tarafından tüketilen tamponlu bir kanala akar.\u003c/p\u003e\n\u003ch2 id=\"2-i-havuzu\"\u003e2. İşçi havuzu\u003c/h2\u003e\n\u003cp\u003eMotor, sabit sayıda \u003cstrong\u003egoroutine\u003c/strong\u003e içeren bir havuz yönetir — her biri \u003ccode\u003e--concurrency\u003c/code\u003e değerine karşılık gelir (varsayılan: CPU sayısı). Her işçi kanaldan bir parça alır ve tespit hattını bağımsız olarak çalıştırır. İşçiler değişebilir durum paylaşmadığından havuz, I/O ve bellek sınırlarına kadar eşzamanlılıkla doğrusal ölçeklenir.\u003c/p\u003e\n\u003cp\u003eTaramalar \u003ccode\u003eSIGINT\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e'e yanıt verir: iptal sinyali geldiğinde bağlam iptal edilir, işçiler mevcut parçalarını tamamlayıp durur ve kısmi sonuçlar çıktı yazılmadan önce toplanır.\u003c/p\u003e\n\u003ch2 id=\"3-aho-corasick-anahtar-kelime-n-filtresi\"\u003e3. Aho-Corasick anahtar kelime ön-filtresi\u003c/h2\u003e\n\u003cp\u003eHer parça üzerinde 64 regex desenini çalıştırmak yavaş olur. Bunun yerine motor, başlangıçta her dedektörün bildirdiği anahtar kelime listelerinden tek bir \u003cstrong\u003eAho-Corasick çok-desenli otomat\u003c/strong\u003e oluşturur. Her parça için bu otomat tek bir doğrusal geçiş yapar ve yalnızca anahtar kelimeleri parçanın baytlarında görünen dedektörleri döndürür.\u003c/p\u003e\n\u003cp\u003eBu, çoğu dedektörün çoğu parça üzerinde regex'ini hiç çalıştırmadığı anlamına gelir. Anahtar kelime bildirmeyen dedektörler her zaman çalışır (ön filtreyi atlayarak doğrudan regex'e geçerler).\u003c/p\u003e\n\u003cp\u003eAho-Corasick uygulaması \u003ca href=\"https://github.com/cloudflare/ahocorasick\"\u003ecloudflare/ahocorasick\u003c/a\u003e kütüphanesinden gelmektedir.\u003c/p\u003e\n\u003ch2 id=\"4-regex-dedektrler\"\u003e4. Regex dedektörler\u003c/h2\u003e\n\u003cp\u003eKısa listeye alınan her dedektör, derlenmiş \u003cstrong\u003edüzenli ifadesini\u003c/strong\u003e parça baytları üzerinde çalıştırır. Bir desen eşleştiğinde dedektör şunları içeren bir \u003ccode\u003eRawFinding\u003c/code\u003e döndürür:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eHam sır baytları (yalnızca doğrulama için bellekte tutulur; asla loglanmaz veya diske yazılmaz).\u003c/li\u003e\n\u003cli\u003eÇıktı için güvenli olan \u003cstrong\u003emaskelenmiş\u003c/strong\u003e bir gösterim.\u003c/li\u003e\n\u003cli\u003eİsteğe bağlı ek meta veri (örneğin bir AWS anahtarı için hesap kimliği).\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eLeakwatch, 60 paket genelinde \u003cstrong\u003e64 yerleşik dedektör\u003c/strong\u003e ile birlikte gelir; bulut sağlayıcılarını, yapay zekâ API'lerini, ödeme platformlarını, veritabanlarını, mesajlaşma araçlarını, sürüm kontrolünü ve daha fazlasını kapsar. \u003ca href=\"#/detectors/custom-rules\"\u003eÖzel YAML kuralları\u003c/a\u003e aracılığıyla kendi desenlerinizi ekleyebilirsiniz.\u003c/p\u003e\n\u003cp\u003eTüm dedektörler, Go'nun \u003ccode\u003einit()\u003c/code\u003e işlevi ve boş importlar kullanılarak derleme zamanında kaydedilir (ADR-0004). Çalışma zamanında eklenti yükleyici veya dinamik keşif yoktur.\u003c/p\u003e\n\u003ch2 id=\"5-satr-ii-ignore-kontrol\"\u003e5. Satır içi ignore kontrolü\u003c/h2\u003e\n\u003cp\u003eBir bulgu doğrulamaya gönderilmeden önce motor, kaynak satırın bir \u003cstrong\u003esatır içi ignore işareti\u003c/strong\u003e içerip içermediğini kontrol eder:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-go\"\u003e// leakwatch:ignore\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eveya dedektöre özgü bir varyant:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-go\"\u003e// leakwatch:ignore:aws-access-key-id\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eİşaret mevcutsa bulgu, \u003cstrong\u003eherhangi bir ağ çağrısı yapılmadan önce\u003c/strong\u003e sessizce bırakılır. Bu kasıtlıdır: yoksayılan sırlar asla canlı bir API isteğini tetiklememeli.\u003c/p\u003e\n\u003ch2 id=\"6-dorulama\"\u003e6. Doğrulama\u003c/h2\u003e\n\u003cp\u003eTüm parçalar için tespit tamamlandıktan sonra motor, bulguları ayrı bir \u003cstrong\u003edoğrulama işçi havuzuna\u003c/strong\u003e geçirir (varsayılan 4 işçi). Doğrulama:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eTüm işçiler arasında paylaşılan global bir \u003cstrong\u003ehız sınırlayıcı\u003c/strong\u003e (varsayılan saniyede 10 istek) ile korunur.\u003c/li\u003e\n\u003cli\u003eHer API çağrısına \u003cstrong\u003eistek başına zaman aşımı\u003c/strong\u003e (varsayılan 10 saniye) uygular.\u003c/li\u003e\n\u003cli\u003eSağlayıcıya yalnızca \u003cstrong\u003esalt-okunur, yıkıcı olmayan\u003c/strong\u003e çağrılar yapar (örneğin AWS anahtarları için \u003ccode\u003ests:GetCallerIdentity\u003c/code\u003e).\u003c/li\u003e\n\u003cli\u003eHer bulguyu dört durumdan biriyle işaretler: \u003ccode\u003everified_active\u003c/code\u003e, \u003ccode\u003everified_inactive\u003c/code\u003e, \u003ccode\u003eunverified\u003c/code\u003e veya \u003ccode\u003everify_error\u003c/code\u003e.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eLeakwatch \u003cstrong\u003e54 doğrulayıcı\u003c/strong\u003e ile birlikte gelir; 64 yerleşik dedektör türünün %84,4'ünü kapsar. Kalan 10 tür (JWT'ler ve genel API anahtarları gibi) güvenli biçimde doğrulanamaz ve her zaman \u003ccode\u003eunverified\u003c/code\u003e olarak raporlanır.\u003c/p\u003e\n\u003cp\u003eBu aşamayı tamamen atlamak için \u003ccode\u003e--no-verify\u003c/code\u003e geçirin — hızlı, çevrimdışı taramalar için kullanışlıdır.\u003c/p\u003e\n\u003cp\u003eDoğrulama davranışı ve durum anlamları hakkında derinlemesine bilgi için \u003ca href=\"#/verification/how-verification-works\"\u003eDoğrulama Nasıl Çalışır\u003c/a\u003e sayfasına bakın.\u003c/p\u003e\n\u003ch2 id=\"7-bulgu-kimlii-ve-entropi\"\u003e7. Bulgu kimliği ve entropi\u003c/h2\u003e\n\u003cp\u003eHer bulgu, şu şekilde hesaplanan \u003cstrong\u003edeterministik bir kimlik\u003c/strong\u003e alır:\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003esha256(detectorID + redacted + filePath + line) → ilk 16 bayta kısaltılır,\n 32 karakterlik bir dizeye hex kodlanır\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eSonuç, düz, 32 karakterlik küçük harfli bir hex dizesidir (örn. \u003ccode\u003e447b5d2846d08ce25dd3d638cfe911ad\u003c/code\u003e) — \u003cstrong\u003eçizgili\u003c/strong\u003e bir UUID \u003cstrong\u003edeğildir\u003c/strong\u003e. Aynı konumdaki aynı sır her zaman aynı kimliği üretir; bu da bulguları çalıştırmalar arasında yinelenenleri kaldırmayı veya sorun izleyicilerde takip etmeyi güvenli kılar.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eShannon entropisi\u003c/strong\u003e (aralık 0–8) her bulgu için hesaplanır ve bilgilendirme amacıyla çıktıda gösterilir. Motor düzeyinde, \u003ccode\u003edetection.entropy.threshold\u003c/code\u003e kapısı yalnızca açıkça buna dahil olan sezgisel dedektörlere uygulanır — şu anda yalnızca \u003ccode\u003egeneric-api-key\u003c/code\u003e — entropisi eşiğin altına düşen bir eşleşmeyi, düşük rastgelelikli yer tutucuları bastırmak için düşürür. \u003ccode\u003eaws-access-key-id\u003c/code\u003e veya \u003ccode\u003egithub-token\u003c/code\u003e gibi her yapısal (biçim-çapalı) dedektör entropi tarafından hiçbir zaman kapılanmaz: bu dedektörlerden gelen düşük entropili bir eşleşme yine de sonuçlarda görünür. Özel kurallar, bu motor düzeyindeki kapıdan ayrı olarak kendi bağımsız kural başına \u003ccode\u003eentropy\u003c/code\u003e eşiğini uygular (bkz. \u003ca href=\"#/detectors/custom-rules\"\u003eÖzel Kurallar\u003c/a\u003e).\u003c/p\u003e\n\u003ch2 id=\"8-tarama-sonras-filtreler\"\u003e8. Tarama sonrası filtreler\u003c/h2\u003e\n\u003cp\u003eDoğrulamadan sonra iki filtre uygulanır:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e--only-verified\u003c/code\u003e — \u003ccode\u003everified_active\u003c/code\u003e olmayan tüm bulguları bırakır.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e--min-severity\u003c/code\u003e — belirtilen önem düzeyinin (\u003ccode\u003elow\u003c/code\u003e | \u003ccode\u003emedium\u003c/code\u003e | \u003ccode\u003ehigh\u003c/code\u003e | \u003ccode\u003ecritical\u003c/code\u003e; varsayılan \u003ccode\u003elow\u003c/code\u003e) altındaki bulguları bırakır.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eHer iki filtre de doğrulama sonrasında çalışır; böylece \u003ccode\u003e--only-verified\u003c/code\u003e değerlendirildiğinde doğrulama durumu kullanılabilir olur.\u003c/p\u003e\n\u003ch2 id=\"9-kt\"\u003e9. Çıktı\u003c/h2\u003e\n\u003cp\u003eHayatta kalan bulgular beş \u003cstrong\u003ebiçimleyiciden\u003c/strong\u003e birine iletilir:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBiçim\u003c/th\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eYaygın kullanım\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003eJSON\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e--format json\u003c/code\u003e (varsayılan)\u003c/td\u003e\n\u003ctd\u003eMakine tarafından okunabilir, hat dostu\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eSARIF v2.1.0\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e--format sarif\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGitHub Code Scanning, güvenlik panoları\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eCSV\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e--format csv\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eElektronik tablolar, veri analizi\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eTablo\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e--format table\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTerminal incelemesi, önem derecesine göre renklendirilmiş\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eGitHub ek açıklamaları\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e--format github\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGitHub Actions'ta satır içi pull request ek açıklamaları\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eÇıktı varsayılan olarak stdout'a gider; bir dosyaya yazmak için \u003ccode\u003e--output \u0026lt;dosya\u0026gt;\u003c/code\u003e kullanın.\u003c/p\u003e\n\u003cp\u003eBiçim veya çıktı hedefi ne olursa olsun, her taramadan sonra bir \u003cstrong\u003etarama özeti\u003c/strong\u003e (tarih, kaynak türü, hedef, taranan dosyalar, süre, bulgu sayısı, kesme durumu) her zaman \u003cstrong\u003estderr\u003c/strong\u003e'e yazdırılır.\u003c/p\u003e\n\u003ch2 id=\"sr-gvenlii\"\u003eSır güvenliği\u003c/h2\u003e\n\u003cp\u003eLeakwatch, bulunan sırların doğrulama çağrıları dışında süreç sınırını asla terk etmemesi için tasarlanmıştır:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eHam sır baytları yalnızca tespit ve doğrulama sırasında bellekte yaşar.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e--show-raw\u003c/code\u003e bayrağı varsayılan olarak \u003ccode\u003efalse\u003c/code\u003e'tur; bu olmadan çıktıda yalnızca maskelenmiş gösterim görünür.\u003c/li\u003e\n\u003cli\u003eSırlar asla diske yazılmaz, \u003ccode\u003eslog\u003c/code\u003e aracılığıyla loglanmaz veya çalıştırmalar arasında önbelleğe alınmaz.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"tasarm-kararlar\"\u003eTasarım kararları\u003c/h2\u003e\n\u003cp\u003eMimari, ADR'ler olarak belgelenmiş çeşitli bilinçli seçimleri yansıtır:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eGo + CGO devre dışı\u003c/strong\u003e (ADR-0001) — tek statik ikili dosya, çalışma zamanı bağımlılığı yok, tüm platformlara çapraz derlenir.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCobra + Viper\u003c/strong\u003e (ADR-0002) — \u003ccode\u003ebayrak \u0026gt; env \u0026gt; yapılandırma \u0026gt; varsayılan\u003c/code\u003e önceliğiyle hiyerarşik CLI.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ego-git\u003c/strong\u003e (ADR-0003) — saf Go Git kütüphanesi; harici \u003ccode\u003egit\u003c/code\u003e ikili dosyası gerekmez.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDerleme zamanı dedektör kaydı\u003c/strong\u003e (ADR-0004) — \u003ccode\u003einit()\u003c/code\u003e + boş importlar; tür güvenli, çalışma zamanı eklenti yükleyicisi yok.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAho-Corasick hibrit eşleştirme\u003c/strong\u003e (ADR-0005) — ön filtre, alakasız parçalardaki regex çalışmasının çoğunu ortadan kaldırır.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ego-containerregistry\u003c/strong\u003e (ADR-0006) — daemonsuz katman analizi; imajları taramak için Docker daemon gerekmez.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eİşçi havuzu\u003c/strong\u003e (ADR-0008) — sabit goroutine sayısı, kanal tabanlı fan-out; öngörülebilir bellek ve CPU kullanımı.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eHızlı Başlangıç\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eDoğrulama Nasıl Çalışır\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Referansı\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/detectors/custom-rules\"\u003eÖzel Kurallar\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n"},"getting-started/installation":{"title":"Kurulum","description":"Leakwatch'ı Homebrew, go install, Docker veya hazır bir ikili dosya ile kurun.","html":"\u003ch1 id=\"kurulum\"\u003eKurulum\u003c/h1\u003e\n\u003cp\u003eLeakwatch'ı makinenize kurmak bir dakikadan az sürer. İş akışınıza en uygun yöntemi seçin: Homebrew macOS ve Linux'ta en basit seçenektir, \u003ccode\u003ego install\u003c/code\u003e halihazırda bir Go araç zinciriniz varsa idealdir, Docker ana sisteminizi temiz tutar ve hazır ikili dosyalar herhangi bir araç zinciri gerektirmeden her yerde çalışır.\u003c/p\u003e\n\u003ch2 id=\"homebrew-macos-ve-linux\"\u003eHomebrew (macOS ve Linux)\u003c/h2\u003e\n\u003cp\u003eResmi tap, amd64 ve arm64 mimarilerinde macOS ve Linux'u destekler.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003ebrew install HodeTech/tap/leakwatch\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eTap, \u003ca href=\"https://github.com/HodeTech/homebrew-tap\"\u003egithub.com/HodeTech/homebrew-tap\u003c/a\u003e adresinde barındırılmaktadır. Homebrew ile yükseltmek için \u003ccode\u003ebrew upgrade leakwatch\u003c/code\u003e komutunu kullanın.\u003c/p\u003e\n\u003ch2 id=\"go-install\"\u003ego install\u003c/h2\u003e\n\u003cp\u003eGo 1.25 veya daha yeni bir sürümü yüklüyse, en son sürümü doğrudan kaynaktan derleyip kurabilirsiniz:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003ego install github.com/HodeTech/leakwatch@latest\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eİkili dosya \u003ccode\u003e$(go env GOPATH)/bin\u003c/code\u003e dizinine yerleştirilir. Bu dizinin \u003ccode\u003ePATH\u003c/code\u003e değişkeninde olduğundan emin olun.\u003c/p\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNot\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003e\u003ccode\u003ego install\u003c/code\u003e her zaman en son etiketli sürümü getirir. Belirli bir sürüme sabitlemek için \u003ccode\u003e@latest\u003c/code\u003e yerine \u003ccode\u003e@v1.5.0\u003c/code\u003e gibi bir etiket kullanın.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"docker\"\u003eDocker\u003c/h2\u003e\n\u003cp\u003eMinimal, çok aşamalı bir Alpine imajı GitHub Container Registry'de yayımlanmaktadır. İmaj, root olmayan bir kullanıcı (\u003ccode\u003eleakwatch\u003c/code\u003e) olarak çalışır, CGO devre dışıdır ve çalışma dizini olarak \u003ccode\u003e/scan\u003c/code\u003e kullanır.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003edocker run --rm \\\n -v \u0026quot;$(pwd):/scan\u0026quot; \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan fs /scan\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eKullanılabilir etiketler:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eEtiket\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e:latest\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eEn son sürüm\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e:v1.5.0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTam sürüm sabitleme\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e:v1.5\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eKüçük sürüm sabitleme (yama sürümlerini takip eder)\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eTaramak istediğiniz dizini konteyner içindeki \u003ccode\u003e/scan\u003c/code\u003e dizinine bağlayın. Bayraklar ve seçenekler yerel ikili dosyayla tamamen aynı şekilde çalışır — tam liste için \u003ca href=\"#/reference/cli-reference\"\u003eCLI Referansı\u003c/a\u003e sayfasına bakın.\u003c/p\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eİpucu\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eUzak Git depolarını tarama ve kimlik bilgilerini güvenli biçimde geçirme dahil Docker'a özgü kullanım kalıpları için \u003ca href=\"#/ci-cd/docker-usage\"\u003eDocker Kullanımı\u003c/a\u003e sayfasına bakın.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"hazr-ikili-dosya\"\u003eHazır ikili dosya\u003c/h2\u003e\n\u003cp\u003eHer sürüm, desteklenen tüm platformlar için \u003ca href=\"https://github.com/HodeTech/Leakwatch/releases\"\u003eGitHub Releases\u003c/a\u003e sayfasında tar arşivleri yayımlar. Platformunuza ait arşivi indirin, açın ve ikili dosyayı \u003ccode\u003ePATH\u003c/code\u003e değişkeninizdeki bir dizine taşıyın.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eDesteklenen platformlar:\u003c/strong\u003e amd64 ve arm64 mimarilerinde Linux, macOS ve Windows.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Linux amd64 örneği — OS ve ARCH değerlerini platformunuza göre değiştirin\ncurl -LO https://github.com/HodeTech/Leakwatch/releases/latest/download/leakwatch_Linux_amd64.tar.gz\ntar -xzf leakwatch_Linux_amd64.tar.gz\nsudo mv leakwatch /usr/local/bin/leakwatch\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003ePlatform adlandırması \u003ccode\u003eleakwatch_\u0026lt;OS\u0026gt;_\u0026lt;ARCH\u0026gt;.tar.gz\u003c/code\u003e kalıbını izler; \u003ccode\u003e\u0026lt;OS\u0026gt;\u003c/code\u003e değeri \u003ccode\u003eLinux\u003c/code\u003e, \u003ccode\u003eDarwin\u003c/code\u003e veya \u003ccode\u003eWindows\u003c/code\u003e, \u003ccode\u003e\u0026lt;ARCH\u0026gt;\u003c/code\u003e değeri ise \u003ccode\u003eamd64\u003c/code\u003e veya \u003ccode\u003earm64\u003c/code\u003e olabilir.\u003c/p\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eİpucu\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eHer sürüm ayrıca her arşiv için bir Yazılım Malzeme Listesi (SBOM) ve sürümün sağlama toplamları dosyası üzerinde bir \u003ca href=\"https://github.com/sigstore/cosign\"\u003eSigstore/cosign\u003c/a\u003e anahtarsız imza (\u003ccode\u003echecksums.txt.sig\u003c/code\u003e / \u003ccode\u003echecksums.txt.pem\u003c/code\u003e) yayımlar; böylece indirilen bir ikili dosyanın kökeni, tedarik zinciri açısından hassas ortamlarda doğrulanabilir.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"kurulumu-dorulama\"\u003eKurulumu doğrulama\u003c/h2\u003e\n\u003cp\u003eHerhangi bir kurulum yönteminin ardından ikili dosyanın erişilebilir olduğunu doğrulayın ve sürümü kontrol edin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch version\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBeklenen çıktı:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eleakwatch v1.5.0 (commit: a3f9c12, built: 2026-05-10T08:22:00Z)\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eKomut bulunamazsa kurulum dizininin \u003ccode\u003ePATH\u003c/code\u003e değişkeninde olup olmadığını kontrol edin.\u003c/p\u003e\n\u003ch2 id=\"sonraki-admlar\"\u003eSonraki adımlar\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eHızlı Başlangıç\u003c/a\u003e — ilk taramanızı bir dakikadan kısa sürede çalıştırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/how-it-works\"\u003eNasıl Çalışır\u003c/a\u003e — Leakwatch taramasının arkasındaki mimari.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e — \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e ile tarama davranışını özelleştirin.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eHızlı Başlangıç\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/docker-usage\"\u003eDocker Kullanımı\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Referansı\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n"},"getting-started/introduction":{"title":"Tanıtım","description":"Leakwatch nedir, neyi tarar ve sızan sırları nasıl tespit edip doğrular.","html":"\u003ch1 id=\"tantm\"\u003eTanıtım\u003c/h1\u003e\n\u003cp\u003e\u003cstrong\u003eLeakwatch\u003c/strong\u003e, sızan sırları — API anahtarları, token'lar, parolalar, bağlantı dizeleri ve özel anahtarlar — kod tabanlarınızda, Git geçmişinizde, konteyner imajlarınızda, bulut depolamanızda ve Slack çalışma alanlarınızda \u003cstrong\u003etespit eden, doğrulayan ve raporlayan\u003c/strong\u003e yüksek performanslı, açık kaynaklı (MIT) bir güvenlik aracıdır.\u003c/p\u003e\n\u003cp\u003eGo ile yazılmıştır, çalışma zamanı bağımlılığı olmayan tek bir statik ikili dosya olarak dağıtılır (\u003ccode\u003eCGO_ENABLED=0\u003c/code\u003e) ve her yerde çalışacak şekilde tasarlanmıştır: bir geliştirici dizüstü bilgisayarı, bir pre-commit kancası veya bir CI/CD hattı.\u003c/p\u003e\n\u003ch2 id=\"neden-leakwatch\"\u003eNeden Leakwatch\u003c/h2\u003e\n\u003cp\u003eTek bir commit'te sızan bir kimlik bilgisi — sonradan silinse bile — Git geçmişinde sonsuza dek erişilebilir kalabilir ve push edildikten dakikalar sonra istismar edilebilir. Leakwatch, bu sırları erken yakalamak ve hangilerinin \u003cem\u003egerçekten tehlikeli\u003c/em\u003e olduğunu söylemek için tasarlanmıştır:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eGeniş tespit\u003c/strong\u003e — bulut sağlayıcılarını, yapay zekâ API'lerini, ödeme platformlarını, veritabanlarını, mesajlaşma araçlarını ve daha fazlasını kapsayan 64 yerleşik dedektör; ayrıca kendi YAML özel kurallarınız.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eYalnızca tespit değil, doğrulama\u003c/strong\u003e — 54 dedektör türü için Leakwatch, bulunan bir sırrın \u003cem\u003ehâlâ etkin\u003c/em\u003e olup olmadığını sağlayıcıya kontrollü, salt-okunur bir çağrı yaparak teyit edebilir. Etkin olduğu doğrulanmış bir anahtar bir olaydır; etkin olmayan bir anahtar ise gürültüdür.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eÇok sayıda kaynak\u003c/strong\u003e — yerel dosya sistemi, eksiksiz bir Git geçmişi, bir OCI/Docker imajı, AWS S3, Google Cloud Storage ve Slack mesajları.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCI-uyumlu çıktı\u003c/strong\u003e — JSON, SARIF (GitHub Code Scanning için), CSV, renklendirilmiş bir terminal tablosu ve satır içi GitHub Actions ek açıklamaları.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTasarımı gereği sır-güvenli\u003c/strong\u003e — bulunan sırlar varsayılan olarak maskelenir ve asla loglanmaz, önbelleğe alınmaz veya diske yazılmaz.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"neleri-tarar\"\u003eNeleri tarar\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eKaynak\u003c/th\u003e\n\u003cth\u003eKomut\u003c/th\u003e\n\u003cth\u003eNeyi kapsar\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003eDosya sistemi\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eleakwatch scan fs\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYerel bir dizin ağacındaki dosyalar\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eGit geçmişi\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eleakwatch scan git\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTüm commit geçmişindeki her blob (yerel veya uzak)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eKonteyner imajı\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eleakwatch scan image\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOCI/Docker imaj katmanları, daemonsuz\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eAWS S3\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eleakwatch scan s3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBir S3 kovasındaki nesneler\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eGoogle Cloud Storage\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eleakwatch scan gcs\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBir GCS kovasındaki nesneler\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eSlack\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eleakwatch scan slack\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eKanallardaki ve (isteğe bağlı) DM'lerdeki mesaj metni\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eÇoklu depo\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eleakwatch scan repos\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAynı anda birden fazla Git deposu\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"tespit-ksaca-nasl-alr\"\u003eTespit kısaca nasıl çalışır\u003c/h2\u003e\n\u003cp\u003eLeakwatch, büyük girdilerde bile hızlı kalmak için katmanlı bir hat kullanır:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eAho-Corasick anahtar kelime ön-filtresi\u003c/strong\u003e — tek bir çok-desenli otomat, bir parçayı hangi dedektörlerin eşleştirebileceğine hızla karar verir; böylece dedektörlerin çoğu regex'ini hiç çalıştırmaz.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRegex doğrulaması\u003c/strong\u003e — yalnızca kısa listeye alınan dedektörler kesin desenlerini çalıştırır.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eEntropi\u003c/strong\u003e — Shannon entropisi gösterim için hesaplanır ve yerleşik \u003ccode\u003egeneric-api-key\u003c/code\u003e dedektörü ile kendi entropi eşiğine sahip her özel kuralı kapılayarak düşük rastgelelikteki yer tutucu eşleşmeleri eler. Diğer her (yapısal) yerleşik dedektör entropi tarafından hiçbir zaman kapılanmaz.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDoğrulama\u003c/strong\u003e — uygun bulgular canlı sağlayıcı API'sine karşı kontrol edilir.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eİpucu\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eLeakwatch'ı kullanmak için bu hattı anlamanız gerekmez — ancak taramaların neden hızlı olduğunu ve bazı bulguların neden bir doğrulama durumu gösterirken bazılarının göstermediğini açıklar. Tam tablo için \u003ca href=\"#/getting-started/how-it-works\"\u003eNasıl Çalışır\u003c/a\u003e bölümüne bakın.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"leakwatch-ne-deildir\"\u003eLeakwatch \u003cem\u003ene değildir\u003c/em\u003e\u003c/h2\u003e\n\u003cp\u003eBeklentileri doğru belirlemek için:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eGit geçmişini yeniden yazmaz veya sırları sizin için \u003cstrong\u003ekaldırmaz\u003c/strong\u003e — onları bulup raporlar ve (\u003ccode\u003e--remediation\u003c/code\u003e ile) nasıl döndüreceğinizi söyler.\u003c/li\u003e\n\u003cli\u003eSlack taraması yalnızca \u003cstrong\u003emesaj metnini\u003c/strong\u003e kapsar; yüklenen dosyaların \u003cem\u003eiçeriğini\u003c/em\u003e taramak uygulanmamıştır.\u003c/li\u003e\n\u003cli\u003eDoğrulama, birçok sır türü için mevcuttur ancak hepsi için değil — 10 dedektör türü (JWT'ler ve genel API anahtarları gibi) güvenli biçimde doğrulanamaz ve her zaman doğrulanmamış olarak raporlanır.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"sonraki-admlar\"\u003eSonraki adımlar\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/installation\"\u003eKurulum\u003c/a\u003e — Homebrew, \u003ccode\u003ego install\u003c/code\u003e, Docker veya hazır bir ikili dosya ile kurun.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eHızlı Başlangıç\u003c/a\u003e — ilk taramanızı bir dakikadan kısa sürede çalıştırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/how-it-works\"\u003eNasıl Çalışır\u003c/a\u003e — taramanın arkasındaki mimari.\u003c/li\u003e\n\u003c/ul\u003e\n"},"getting-started/quick-start":{"title":"Hızlı Başlangıç","description":"İlk Leakwatch taramanızı bir dakikadan kısa sürede çalıştırın.","html":"\u003ch1 id=\"hzl-balang\"\u003eHızlı Başlangıç\u003c/h1\u003e\n\u003cp\u003eLeakwatch'ın neler yapabileceğini anlamanın en hızlı yolu, onu gerçek bir dizine yönlendirmektir. Bu sayfa ilk taramanızda size rehberlik eder, çıktının ne anlama geldiğini açıklar ve en sık kullanacağınız bayrakları gösterir.\u003c/p\u003e\n\u003ch2 id=\"n-koullar\"\u003eÖn koşullar\u003c/h2\u003e\n\u003cp\u003eLeakwatch kurulu ve \u003ccode\u003ePATH\u003c/code\u003e değişkeninizde erişilebilir olmalıdır. Henüz yapmadıysanız \u003ca href=\"#/getting-started/installation\"\u003eKurulum\u003c/a\u003e sayfasına bakın.\u003c/p\u003e\n\u003ch2 id=\"lk-taramanz\"\u003eİlk taramanız\u003c/h2\u003e\n\u003cp\u003eMevcut dizini tek bir komutla tarayın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs .\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eVarsayılan olarak çıktı JSON biçiminde stdout'a yazılır. Bunun yerine okunabilir, renklendirilmiş bir tablo almak için \u003ccode\u003e--format table\u003c/code\u003e ekleyin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBir sonucun nasıl göründüğü aşağıdadır (tablo formatlayıcısı her zaman sondaki REMEDIATION sütununu yazdırır; \u003ccode\u003e--remediation\u003c/code\u003e de ayarlanmadıkça \u003ccode\u003e-\u003c/code\u003e gösterir):\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eSEVERITY DETECTOR FILE LINE REDACTED STATUS REMEDIATION\n-------- -------- ---- ---- -------- ------ -----------\nCRITICAL aws-access-key-id config/deploy.env 12 ****MNOP verified_active -\nHIGH npm-token scripts/bootstrap.sh 37 npm_****wxyz verified_active -\nMEDIUM generic-api-key src/services/analytics.js 89 ****w2y4 unverified -\n\nFound 3 secrets (1 critical, 1 high, 1 medium).\n\n── Scan Summary ─────────────────────────────────\n Date: 2026-05-23 14:03:11\n Source: filesystem\n Target: /home/user/myproject\n Files scanned: 312\n Duration: 1.24s\n Findings: 3\n─────────────────────────────────────────────────\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eTarama özeti her zaman \u003cstrong\u003estderr\u003c/strong\u003e'e yazdırılır; bu nedenle pipe veya yeniden yönlendirilen çıktıyla hiçbir zaman çakışmaz.\u003c/p\u003e\n\u003ch2 id=\"bulguyu-anlamak\"\u003eBulguyu anlamak\u003c/h2\u003e\n\u003cp\u003eTablodaki her satır (veya JSON'daki her nesne) bir bulguyu temsil eder. Temel alanlar şunlardır:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eAlan\u003c/th\u003e\n\u003cth\u003eAnlam\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eSEVERITY\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003eSır türünün ne kadar kritik olduğu: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e veya \u003ccode\u003ecritical\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eDETECTOR\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003eEşleşen dedektör — sır türünü tanımlar (örneğin \u003ccode\u003eaws-access-key-id\u003c/code\u003e)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eFILE\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003eSırrın bulunduğu dosyanın tarama köküne göreli yolu\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eLINE\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003eEşleşmenin satır numarası\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eREDACTED\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003eSırrın maskelenmiş gösterimi — \u003ccode\u003e--show-raw\u003c/code\u003e ayarlanmadıkça ham değer hiçbir zaman gösterilmez\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eSTATUS\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003eDoğrulama sonucu: \u003ccode\u003everified_active\u003c/code\u003e, \u003ccode\u003everified_inactive\u003c/code\u003e, \u003ccode\u003eunverified\u003c/code\u003e veya \u003ccode\u003everify_error\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eREMEDIATION\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003eDöndürme/iptal rehberi başlığı, veya \u003ccode\u003e--remediation\u003c/code\u003e geçilmediğinde \u003ccode\u003e-\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003ccode\u003everified_active\u003c/code\u003e durumu, Leakwatch'ın sağlayıcıya salt-okunur bir API çağrısı yaparak sırrın hâlâ etkin olduğunu doğruladığı anlamına gelir. \u003cstrong\u003eHer \u003ccode\u003everified_active\u003c/code\u003e bulgusunu açık bir olay olarak değerlendirin.\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"yaygn-tarama-seenekleri\"\u003eYaygın tarama seçenekleri\u003c/h2\u003e\n\u003ch3 id=\"yalnzca-onaylanm-srlara-odaklann\"\u003eYalnızca onaylanmış sırlara odaklanın\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --only-verified\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBu seçenek doğrulanmamış ve etkin olmayan bulguları gizler; yalnızca etkin olduğu onaylananları bırakır. Çok sayıda sonucunuz olduğunda önceliklendirme için kullanışlıdır.\u003c/p\u003e\n\u003ch3 id=\"hzl-evrimd-tarama-iin-a-dorulamasn-atlayn\"\u003eHızlı çevrimdışı tarama için ağ doğrulamasını atlayın\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --no-verify\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eDoğrulama tamamen atlanır — hiçbir giden ağ çağrısı yapılmaz. Sonuçlar daha hızlı görünür ve internet bağlantısı olmadan çalışır, ancak tüm bulgular \u003ccode\u003eunverified\u003c/code\u003e olarak işaretlenir.\u003c/p\u003e\n\u003ch3 id=\"dzeltme-klavuzu-ekleyin\"\u003eDüzeltme kılavuzu ekleyin\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --remediation --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eHer bulgu, söz konusu sır türünü nasıl döndüreceğinizi veya iptal edeceğinizi açıklayan bir \u003cstrong\u003eREMEDIATION\u003c/strong\u003e sütunu kazanır. Bayrak ayarlandığında aynı veriler JSON, SARIF ve CSV çıktısına da dahil edilir.\u003c/p\u003e\n\u003ch3 id=\"minimum-nem-derecesine-gre-filtreleyin\"\u003eMinimum önem derecesine göre filtreleyin\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --min-severity high\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eYalnızca \u003ccode\u003ehigh\u003c/code\u003e veya \u003ccode\u003ecritical\u003c/code\u003e önem derecesindeki bulgular raporlanır.\u003c/p\u003e\n\u003ch3 id=\"sonular-dosyaya-kaydedin\"\u003eSonuçları dosyaya kaydedin\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --format sarif --output results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003e--output\u003c/code\u003e / \u003ccode\u003e-o\u003c/code\u003e bayrağı stdout yerine bir dosyaya yazar. SARIF çıktısı \u003ca href=\"https://docs.github.com/en/code-security/code-scanning\"\u003eGitHub Code Scanning\u003c/a\u003e ile uyumludur.\u003c/p\u003e\n\u003ch2 id=\"yaplandrma-dosyas-oluturma\"\u003eYapılandırma dosyası oluşturma\u003c/h2\u003e\n\u003cp\u003eİlk denemede varsayılanlarla çalıştırmak uygundur; ancak tekrarlayan kullanım için proje düzeyinde bir yapılandırma isteyeceksiniz:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch init\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBu komut, eşzamanlılık, entropi, doğrulama, çıktı biçimi ve yaygın yol dışlamaları için önerilen varsayılanlarla mevcut dizine \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e yazar. Mevcut bir dosyanın üzerine yazmak için \u003ccode\u003e--force\u003c/code\u003e, farklı bir yola yazmak için \u003ccode\u003e--output\u003c/code\u003e kullanın.\u003c/p\u003e\n\u003cp\u003eHer seçeneğin tam açıklaması için \u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e sayfasına bakın.\u003c/p\u003e\n\u003ch2 id=\"k-kodlar\"\u003eÇıkış kodları\u003c/h2\u003e\n\u003cp\u003eLeakwatch, CI betiklerinin çıktıyı ayrıştırmadan sonuçlara göre hareket edebilmesi için farklı çıkış kodları kullanır:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eKod\u003c/th\u003e\n\u003cth\u003eAnlam\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlandı — bulgu yok\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlandı — bir veya daha fazla sır bulundu\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama bir hata nedeniyle başarısız oldu\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlanmadan (\u003ccode\u003eCtrl+C\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e) kesildi ve henüz hiçbir bulgu raporlanmamıştı\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eTipik bir CI kapısı şöyle görünür:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --only-verified --format sarif --output results.sarif\nif [ $? -eq 1 ]; then\n echo \u0026quot;Etkin sırlar bulundu — derleme başarısız\u0026quot;\n exit 1\nfi\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-warn\"\u003e\u003cdiv class=\"callout-label\"\u003eUyarı\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eÇıkış kodu \u003ccode\u003e1\u003c/code\u003e, etkin filtreleri geçen (\u003ccode\u003e--min-severity\u003c/code\u003e ve \u003ccode\u003e--only-verified\u003c/code\u003e dahil) \u003cem\u003eherhangi bir\u003c/em\u003e bulgu olduğunda döndürülür. Temiz çıkış kodu \u003ccode\u003e0\u003c/code\u003e, hiçbir bulgunun eşleşmediği anlamına gelir — kod tabanında sır olmadığı anlamına gelmez.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"taramay-iptal-etme\"\u003eTaramayı iptal etme\u003c/h2\u003e\n\u003cp\u003eÇalışan bir taramayı iptal etmek için \u003ccode\u003eCtrl+C\u003c/code\u003e tuşuna basın (veya \u003ccode\u003eSIGTERM\u003c/code\u003e gönderin). Leakwatch düzgün biçimde durur: işlemdeki parçalar tamamlanır, kısmi sonuçlar yazılır ve özet \u003ccode\u003eStatus: interrupted (partial results)\u003c/code\u003e olarak gösterilir. Bulgular kesintiye rağmen önceliklidir — kesintiden önce sırlar zaten bulunmuşsa tarama \u003ccode\u003e1\u003c/code\u003e ile çıkar, böylece bu sinyal hiçbir zaman gizlenmez. \u003ccode\u003e3\u003c/code\u003e çıkış kodu yalnızca tarama sıfır bulguyla kesintiye uğradığında döndürülür; bu sayede bir CI hattı tamamlanmamış bir taramayı asla temiz bir geçişle karıştırmaz.\u003c/p\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/installation\"\u003eKurulum\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/how-it-works\"\u003eNasıl Çalışır\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Referansı\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eDoğrulama Nasıl Çalışır\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n"},"output/output-formats":{"title":"Çıktı Formatları","description":"Leakwatch'ın desteklediği beş çıktı formatı — JSON, SARIF, CSV, tablo ve GitHub ek açıklamaları — örnekler ve her birini ne zaman kullanacağınıza dair rehberlik.","html":"\u003ch1 id=\"kt-formatlar\"\u003eÇıktı Formatları\u003c/h1\u003e\n\u003cp\u003eLeakwatch beş çıktı formatını destekler: makine tarafından okunabilir hatlar, güvenlik araç entegrasyonları, elektronik tablo dışa aktarmaları, insan tarafından okunabilir terminal incelemesi ve GitHub Actions ek açıklamaları. \u003ccode\u003e--format\u003c/code\u003e (veya \u003ccode\u003e-f\u003c/code\u003e) ile bir format seçin; stdout yerine bir dosyaya yazmak için \u003ccode\u003e--output\u003c/code\u003e (veya \u003ccode\u003e-o\u003c/code\u003e) kullanın.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --format json\nleakwatch scan fs . --format sarif --output results.sarif\nleakwatch scan fs . --format csv --output findings.csv\nleakwatch scan fs . --format table\nleakwatch scan fs . --format github # GitHub Actions ek açıklamaları (CI kullanımı)\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eVarsayılan format \u003ccode\u003ejson\u003c/code\u003e'dur.\u003c/p\u003e\n\u003ch2 id=\"json\"\u003eJSON\u003c/h2\u003e\n\u003cp\u003eJSON varsayılan format ve en eksiksiz temsil biçimidir. Leakwatch, stdout'a (veya \u003ccode\u003e--output\u003c/code\u003e ile verilen dosyaya) bulgu nesnelerinden oluşan bir JSON \u003cstrong\u003edizisi\u003c/strong\u003e yazar.\u003c/p\u003e\n\u003cp\u003eHam sır değeri, \u003ccode\u003e--show-raw\u003c/code\u003e açıkça ayarlanmadıkça \u003cstrong\u003ehiçbir zaman\u003c/strong\u003e serileştirilmez. \u003ccode\u003e--show-raw\u003c/code\u003e ile her nesneye bir \u003ccode\u003e\u0026quot;raw\u0026quot;\u003c/code\u003e alanı eklenir.\u003c/p\u003e\n\u003ch3 id=\"rnek-ar\"\u003eÖrnek çağrı\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs ./src --format json --output findings.json\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"rnek-bulgu-nesnesi\"\u003eÖrnek bulgu nesnesi\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-json\"\u003e{\n \u0026quot;id\u0026quot;: \u0026quot;447b5d2846d08ce25dd3d638cfe911ad\u0026quot;,\n \u0026quot;detector_id\u0026quot;: \u0026quot;github-token\u0026quot;,\n \u0026quot;severity\u0026quot;: \u0026quot;critical\u0026quot;,\n \u0026quot;redacted\u0026quot;: \u0026quot;ghp_****************************Xk9R\u0026quot;,\n \u0026quot;source\u0026quot;: {\n \u0026quot;source_type\u0026quot;: \u0026quot;filesystem\u0026quot;,\n \u0026quot;file_path\u0026quot;: \u0026quot;scripts/deploy.sh\u0026quot;,\n \u0026quot;line\u0026quot;: 14\n },\n \u0026quot;verification\u0026quot;: {\n \u0026quot;status\u0026quot;: \u0026quot;verified_active\u0026quot;\n },\n \u0026quot;entropy\u0026quot;: 5.82,\n \u0026quot;detected_at\u0026quot;: \u0026quot;2026-05-23T10:15:30Z\u0026quot;\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003eid\u003c/code\u003e, düz 32 karakterlik küçük harfli bir hex dizesi olarak gösterilen, deterministik ve kısaltılmış bir SHA-256 değeridir — bir UUID \u003cstrong\u003edeğildir\u003c/strong\u003e ve hiçbir zaman tire içermez. Nasıl hesaplandığı için \u003ca href=\"#/getting-started/how-it-works\"\u003eNasıl Çalışır\u003c/a\u003e sayfasına bakın.\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003e--remediation\u003c/code\u003e de ayarlandığında her bulgunun içine iç içe bir \u003ccode\u003e\u0026quot;remediation\u0026quot;\u003c/code\u003e nesnesi yerleştirilir. Bkz. \u003ca href=\"#/output/remediation\"\u003eDüzeltme Rehberi\u003c/a\u003e.\u003c/p\u003e\n\u003ch2 id=\"sarif\"\u003eSARIF\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003esarif\u003c/code\u003e formatı, \u003ca href=\"https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/uploading-a-sarif-file-to-github\"\u003eGitHub Code Scanning\u003c/a\u003e'e yüklenmek üzere tasarlanmış bir SARIF v2.1.0 belgesi üretir. Araç adı \u003ccode\u003eLeakwatch\u003c/code\u003e'tır ve \u003ccode\u003einformationUri\u003c/code\u003e \u003ccode\u003ehttps://github.com/HodeTech/Leakwatch\u003c/code\u003e adresine işaret eder.\u003c/p\u003e\n\u003cp\u003eBulgularda görünen her dedektör, SARIF sürücüsünde bir \u003cstrong\u003ekural\u003c/strong\u003e haline gelir; \u003ccode\u003e--remediation\u003c/code\u003e ayarlandığında düzeltme adımlarından doldurulan \u003ccode\u003ehelp\u003c/code\u003e metni ve sağlayıcı belgelerine işaret eden bir \u003ccode\u003ehelpUri\u003c/code\u003e ile birlikte. Sonuçlar, dedektör ID'si, maskelenmiş değer ve dosya yolundan hesaplanan bir \u003ccode\u003eleakwatch/v1\u003c/code\u003e kısmi parmak izi taşır — bu, çevresindeki kod kaydığında bile GitHub Code Scanning'in aynı uyarıyı takip etmesini sağlar.\u003c/p\u003e\n\u003ch3 id=\"rnek-ar-1\"\u003eÖrnek çağrı\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --format sarif --output results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"github-code-scanninge-ykleme\"\u003eGitHub Code Scanning'e yükleme\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e# Bir GitHub Actions iş akışı adımında:\n- name: SARIF sonuçlarını yükle\n uses: github/codeql-action/upload-sarif@v3\n with:\n sarif_file: results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eTam CI kurulumu için \u003ca href=\"#/ci-cd/github-action\"\u003eGitHub Action\u003c/a\u003e bölümüne bakın.\u003c/p\u003e\n\u003ch2 id=\"csv\"\u003eCSV\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003ecsv\u003c/code\u003e formatı, bir başlık satırı ve ardından bulgu başına bir satır yazar; standart virgülle ayrılmış değerler kullanır. Her hücre yazılmadan önce elektronik tablo formül enjeksiyonuna karşı sterilize edilir.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSütunlar (varsayılan):\u003c/strong\u003e\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eid,detector_id,severity,redacted,source,file_path,line,commit,verification_status,remediation\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003esource\u003c/code\u003e, bir bulgunun geldiği kaynak depoyu, konteyner imajını veya Slack kanalını tanımlayan, insan tarafından okunabilir bir etikettir (bir \u003ccode\u003escan repos\u003c/code\u003e çalıştırması birden fazla depodan gelen bulguları tek bir CSV'de birleştirdiğinde kullanışlıdır); tek bir dizinin düz bir dosya sistemi taraması için boştur. \u003ccode\u003eline\u003c/code\u003e satır numarasıdır, uygulanamadığında boştur.\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003e--show-raw\u003c/code\u003e ayarlandığında, sona bir \u003ccode\u003eraw\u003c/code\u003e sütunu eklenir.\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eremediation\u003c/code\u003e sütunu, \u003ccode\u003e--remediation\u003c/code\u003e ayarlandığında düzeltme başlığını (örn. \u003ccode\u003e\u0026quot;Revoke GitHub Token\u0026quot;\u003c/code\u003e) içerir, aksi hâlde boş kalır.\u003c/p\u003e\n\u003ch3 id=\"rnek-ar-2\"\u003eÖrnek çağrı\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git . --format csv --output findings.csv\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"rnek-kt\"\u003eÖrnek çıktı\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-csv\"\u003eid,detector_id,severity,redacted,source,file_path,line,commit,verification_status,remediation\n447b5d2846d08ce25dd3d638cfe911ad,github-token,critical,ghp_****Xk9R,,scripts/deploy.sh,14,7d3e1f2,verified_active,Revoke GitHub Token\ne6fa909746d7d5242309b64c33209fa9,aws-access-key-id,high,AKIA****K7NP,,config/aws.yml,3,7d3e1f2,unverified,Rotate AWS Access Key\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"tablo\"\u003eTablo\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003etable\u003c/code\u003e formatı, insan tarafından okunabilir sekme hizalı bir tablo yazar; sonuçların hızlı görsel taramasını istediğiniz etkileşimli terminal oturumları için en uygun formattır.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSütunlar:\u003c/strong\u003e\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eSEVERITY | DETECTOR | FILE | LINE | REDACTED | STATUS | REMEDIATION\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003eLINE\u003c/code\u003e sütunu, satır numarası mevcut olmadığında (örneğin bir Slack veya konteyner imajı bulgusu için) \u003ccode\u003e-\u003c/code\u003e gösterir. \u003ccode\u003eREMEDIATION\u003c/code\u003e sütunu her zaman bulunur ve \u003ccode\u003e--remediation\u003c/code\u003e ayarlanmadıkça \u003ccode\u003e-\u003c/code\u003e gösterir. \u003ccode\u003e--show-raw\u003c/code\u003e ayarlandığında da, sona bir \u003ccode\u003eRAW\u003c/code\u003e sütunu eklenir. Tablonun altına bir özet satırı yazdırılır (örn. \u003ccode\u003eFound 3 secrets (1 critical, 2 high).\u003c/code\u003e).\u003c/p\u003e\n\u003cp\u003eSaldırgan tarafından etkilenebilecek alanlar (dedektör ID'si, dosya yolu, maskelenmiş değer), terminale yazılmadan önce kontrol karakterlerinden ve ANSI kaçış dizilerinden arındırılır; böylece kötü amaçlı biçimlendirilmiş bir dosya adı veya özel kural eşleşmesi terminal oturumunuza kaçış dizisi enjekte edemez.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eANSI rengi\u003c/strong\u003e, \u003ccode\u003eSEVERITY\u003c/code\u003e sütununa otomatik olarak uygulanır, ancak yalnızca dört koşulun tamamı sağlandığında:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003ccode\u003e--format table\u003c/code\u003e seçilmiş\u003c/li\u003e\n\u003cli\u003eÇıktı stdout'a gidiyor (\u003ccode\u003e--output \u0026lt;file\u0026gt;\u003c/code\u003e yok)\u003c/li\u003e\n\u003cli\u003estdout bir TTY (pipe veya yönlendirme değil)\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eNO_COLOR\u003c/code\u003e ortam değişkeni ayarlanmamış\u003c/li\u003e\n\u003c/ol\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eÖnem derecesi\u003c/th\u003e\n\u003cth\u003eRenk\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecritical\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eKalın kırmızı\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ehigh\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eKırmızı\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003emedium\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSarı\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMavi\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"rnek-ar-3\"\u003eÖrnek çağrı\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --format table --min-severity high\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"rnek-kt-1\"\u003eÖrnek çıktı\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eSEVERITY DETECTOR FILE LINE REDACTED STATUS REMEDIATION\n-------- -------- ---- ---- -------- ------ -----------\nCRITICAL github-token scripts/deploy.sh 14 ghp_****Xk9R verified_active Revoke GitHub Token\nHIGH aws-access-key-id config/aws.yml 3 AKIA****K7NP unverified Rotate AWS Access Key\n\nFound 2 secrets (1 critical, 1 high).\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"github-ek-aklamalar\"\u003eGitHub ek açıklamaları\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003egithub\u003c/code\u003e formatı, \u003ca href=\"https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions\"\u003eGitHub Actions iş akışı komutlarını\u003c/a\u003e (\u003ccode\u003e::error\u003c/code\u003e / \u003ccode\u003e::warning\u003c/code\u003e / \u003ccode\u003e::notice\u003c/code\u003e) yayar; böylece bulgular bir pull request'in \u003cem\u003eFiles changed\u003c/em\u003e görünümünde ve çalışma günlüğünde \u003cstrong\u003esatır içi ek açıklamalar\u003c/strong\u003e olarak görünür. Runner'ın stdout'una akıtılmak üzere tasarlanmıştır — bir dosyaya yazmanın etkisi yoktur.\u003c/p\u003e\n\u003cp\u003eÖnem derecesi ek açıklama seviyesine eşlenir: \u003ccode\u003ecritical\u003c/code\u003e → \u003ccode\u003eerror\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e → \u003ccode\u003ewarning\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e/\u003ccode\u003elow\u003c/code\u003e → \u003ccode\u003enotice\u003c/code\u003e. Dosya yolu olan bir bulgu o dosya ve satıra bağlanır; dosya yolu olmayan bir bulgu çalışma düzeyinde (run-level) bir ek açıklama olur.\u003c/p\u003e\n\u003cp\u003eGüvenlik için bu format ham sırrı \u003cstrong\u003easla\u003c/strong\u003e yazdırmaz — \u003ccode\u003e--show-raw\u003c/code\u003e ile bile yalnızca maskelenmiş değer gösterilir; çünkü ek açıklamalar (çoğu zaman herkese açık olan) PR arayüzünde ve günlüklerde görüntülenir.\u003c/p\u003e\n\u003ch3 id=\"rnek-ar-4\"\u003eÖrnek çağrı\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --format github\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"rnek-kt-2\"\u003eÖrnek çıktı\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003e::error file=config/prod.env,line=12,title=Leakwatch%3A aws-access-key-id::Potential secret detected by aws-access-key-id (critical): AKIA****K7NP\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBu format normalde elle çağrılmak yerine \u003ca href=\"#/ci-cd/github-action\"\u003eGitHub Action\u003c/a\u003e (\u003ccode\u003eformat: github\u003c/code\u003e) tarafından kullanılır.\u003c/p\u003e\n\u003ch2 id=\"yaygn-kt-bayraklar\"\u003eYaygın çıktı bayrakları\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eKısa\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-f\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktı formatı: \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, \u003ccode\u003egithub\u003c/code\u003e (varsayılan \u003ccode\u003ejson\u003c/code\u003e)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-o\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estdout yerine dosyaya yaz\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--show-raw\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktıya maskelenmemiş sır değerini dahil et\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003c/td\u003e\n\u003ctd\u003eBu önem seviyesinin altındaki bulguları bırak\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003c/td\u003e\n\u003ctd\u003eYalnızca \u003ccode\u003everified_active\u003c/code\u003e bulgularını tut\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--remediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003c/td\u003e\n\u003ctd\u003eBulguları sağlayıcı düzeltme rehberiyle zenginleştir\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/output/remediation\"\u003eDüzeltme Rehberi\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/github-action\"\u003eGitHub Action\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eDoğrulama Nasıl Çalışır\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n"},"output/remediation":{"title":"Düzeltme Rehberi","description":"Bulguları sağlayıcıya özgü döndürme ve iptal adımları, aciliyet dereceleri ve resmi dokümantasyon bağlantılarıyla zenginleştirmek için --remediation kullanın.","html":"\u003ch1 id=\"dzeltme-rehberi\"\u003eDüzeltme Rehberi\u003c/h1\u003e\n\u003cp\u003eBir sırrın sızdığını bilmek işin yalnızca yarısıdır — ayrıca ne yapacağınızı da bilmeniz gerekir. Herhangi bir tarama komutuna \u003ccode\u003e--remediation\u003c/code\u003e eklemek, her bulguyu yapılandırılmış, sağlayıcıya özgü rehberlikle zenginleştirir: kimlik bilgisini döndürme veya iptal etme adımları, sağlayıcının belgelerine bağlantı, yönetim konsoluna bağlantı, aciliyet derecelendirmesi ve bir doğrulama kontrol listesi.\u003c/p\u003e\n\u003ch2 id=\"nasl-etkinletirilir\"\u003eNasıl etkinleştirilir\u003c/h2\u003e\n\u003cp\u003eHerhangi bir tarama komutuna \u003ccode\u003e--remediation\u003c/code\u003e ekleyin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --remediation\nleakwatch scan git . --remediation --format json\nleakwatch scan image myapp:latest --remediation --format sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eDüzeltme zenginleştirmesi varsayılan olarak devre dışıdır. Bayrak yoksa, her bulgunun \u003ccode\u003eremediation\u003c/code\u003e alanı \u003ccode\u003enull\u003c/code\u003e olur ve fazladan veri alınmaz veya hesaplanmaz.\u003c/p\u003e\n\u003ch2 id=\"ne-ierir\"\u003eNe içerir\u003c/h2\u003e\n\u003cp\u003eHer düzeltme girişi aşağıdaki alanları içerir:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eAlan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003etitle\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDüzeltme eyleminin kısa adı (örn. \u003ccode\u003e\u0026quot;Rotate AWS Access Key\u0026quot;\u003c/code\u003e)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esteps\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSırrı döndürmek veya iptal etmek için sıralı adımlar listesi\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edoc_url\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSağlayıcının resmi kimlik bilgisi yönetimi belgelerine bağlantı\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003econsole_url\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSağlayıcının yönetim konsolu sayfasına doğrudan bağlantı\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eurgency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNe kadar hızlı harekete geçileceği: \u003ccode\u003e\u0026quot;immediate\u0026quot;\u003c/code\u003e, \u003ccode\u003e\u0026quot;high\u0026quot;\u003c/code\u003e veya \u003ccode\u003e\u0026quot;medium\u0026quot;\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003echecklist\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDöndürme sonrası doğrulama adımları (örn. denetim günlüklerini inceleyin, güvenlik ekibini bilgilendirin)\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eLeakwatch, her yerleşik dedektör için bir tane olmak üzere 64 düzeltme girişiyle birlikte gelir. 64 girişin tamamı ikili dosyaya dahildir; rehberliği almak için herhangi bir ağ çağrısı yapılmaz.\u003c/p\u003e\n\u003ch2 id=\"her-formatta-nasl-grnr\"\u003eHer formatta nasıl görünür\u003c/h2\u003e\n\u003cp\u003eZenginleştirme, rehberliği bellekteki bulgu nesnesine ekler. Nasıl göründüğü çıktı formatına bağlıdır:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eJSON\u003c/strong\u003e — tam yapılandırılmış \u003ccode\u003eremediation\u003c/code\u003e nesnesi her bulgunun içine yerleştirilir:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --remediation --format json\n\u003c/code\u003e\u003c/pre\u003e\n\u003cpre\u003e\u003ccode class=\"language-json\"\u003e{\n \u0026quot;id\u0026quot;: \u0026quot;447b5d2846d08ce25dd3d638cfe911ad\u0026quot;,\n \u0026quot;detector_id\u0026quot;: \u0026quot;github-token\u0026quot;,\n \u0026quot;severity\u0026quot;: \u0026quot;critical\u0026quot;,\n \u0026quot;redacted\u0026quot;: \u0026quot;ghp_****************************Xk9R\u0026quot;,\n \u0026quot;source\u0026quot;: {\n \u0026quot;source_type\u0026quot;: \u0026quot;filesystem\u0026quot;,\n \u0026quot;file_path\u0026quot;: \u0026quot;scripts/deploy.sh\u0026quot;,\n \u0026quot;line\u0026quot;: 14\n },\n \u0026quot;verification\u0026quot;: {\n \u0026quot;status\u0026quot;: \u0026quot;verified_active\u0026quot;\n },\n \u0026quot;remediation\u0026quot;: {\n \u0026quot;title\u0026quot;: \u0026quot;Revoke GitHub Token\u0026quot;,\n \u0026quot;steps\u0026quot;: [\n \u0026quot;Go to GitHub Settings \u0026gt; Developer settings \u0026gt; Personal access tokens.\u0026quot;,\n \u0026quot;Revoke the compromised token immediately.\u0026quot;,\n \u0026quot;Create a new token with the minimum required scopes.\u0026quot;,\n \u0026quot;Update all integrations and CI/CD pipelines with the new token.\u0026quot;\n ],\n \u0026quot;doc_url\u0026quot;: \u0026quot;https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens\u0026quot;,\n \u0026quot;console_url\u0026quot;: \u0026quot;https://github.com/settings/tokens\u0026quot;,\n \u0026quot;urgency\u0026quot;: \u0026quot;immediate\u0026quot;,\n \u0026quot;checklist\u0026quot;: [\n \u0026quot;Review the GitHub audit log for unauthorized actions performed with the token.\u0026quot;,\n \u0026quot;Check repository and organization settings for unexpected changes.\u0026quot;,\n \u0026quot;Notify the security team about the exposure.\u0026quot;,\n \u0026quot;Scan for other repositories that may contain the same token.\u0026quot;\n ]\n },\n \u0026quot;entropy\u0026quot;: 5.82,\n \u0026quot;detected_at\u0026quot;: \u0026quot;2026-05-23T10:15:30Z\u0026quot;\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003cstrong\u003eSARIF\u003c/strong\u003e — \u003ccode\u003esteps\u003c/code\u003e alanları, kuralın \u003ccode\u003ehelp.text\u003c/code\u003e alanına yerleştirilir ve \u003ccode\u003edoc_url\u003c/code\u003e, kuralın \u003ccode\u003ehelpUri\u003c/code\u003e'si olarak ayarlanır. Bu, GitHub Code Scanning'in uyarı ayrıntıları panelinde doğrudan görünür.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eCSV\u003c/strong\u003e — yalnızca düzeltme \u003ccode\u003etitle\u003c/code\u003e'ı \u003ccode\u003eremediation\u003c/code\u003e sütununa yazılır. Tam yapılandırılmış rehberlik CSV çıktısına dahil edilmez.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTablo\u003c/strong\u003e — \u003ccode\u003eREMEDIATION\u003c/code\u003e sütununda yalnızca düzeltme \u003ccode\u003etitle\u003c/code\u003e'ı gösterilir.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --remediation --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eSEVERITY DETECTOR FILE LINE REDACTED STATUS REMEDIATION\n-------- -------- ---- ---- -------- ------ -----------\nCRITICAL github-token scripts/deploy.sh 14 ghp_****Xk9R verified_active Revoke GitHub Token\n\nFound 1 secret (1 critical).\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eİpucu\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eOtomatik olay müdahale iş akışları için tam yapılandırılmış rehberliğe ihtiyaç duyduğunuzda \u003ccode\u003e--remediation --format json\u003c/code\u003e kullanın. Terminalde hızlı, insan tarafından okunabilir bir önceliklendirme oturumu için \u003ccode\u003e--remediation --format table\u003c/code\u003e kullanın.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNot\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eZenginleştirme yalnızca \u003ccode\u003e--remediation\u003c/code\u003e ayarlandığında çalışır. Bayrak olmadan, \u003ccode\u003eremediation\u003c/code\u003e alanı JSON ve SARIF çıktısında yoktur, CSV \u003ccode\u003eremediation\u003c/code\u003e sütunu boştur ve tablo \u003ccode\u003eREMEDIATION\u003c/code\u003e sütunu \u003ccode\u003e-\u003c/code\u003e gösterir. Bayrak, orijinal tarama sonuçlarını değiştirmez — bunların üzerine bir katman ekler.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/output/output-formats\"\u003eÇıktı Formatları\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eDoğrulama Nasıl Çalışır\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n"},"reference/cli-reference":{"title":"CLI Başvurusu","description":"Her Leakwatch komutu, alt komutu ve bayrağı için tam başvuru kaynağı.","html":"\u003ch1 id=\"cli-bavurusu\"\u003eCLI Başvurusu\u003c/h1\u003e\n\u003cp\u003eBu sayfa, tüm Leakwatch komutları ve bayrakları için yetkili başvuru kaynağıdır. Kavramsal açıklamalar ve çalışma örnekleri için ilgili tarama veya yapılandırma sayfalarındaki çapraz bağlantıları takip edin.\u003c/p\u003e\n\u003ch2 id=\"global-bayraklar\"\u003eGlobal bayraklar\u003c/h2\u003e\n\u003cp\u003eBu bayraklar her komut ve alt komut üzerinde kullanılabilir.\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--config \u0026lt;path\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOtomatik olarak bulunan \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYapılandırma dosyasının yolu. Atlandığında Leakwatch, \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e için önce geçerli dizini, ardından kullanıcının ana dizinini arar — üst dizin taraması \u003cstrong\u003eyapılmaz\u003c/strong\u003e. Her iki konumdaki bozuk bir yapılandırma dosyası (geçersiz YAML) da ölümcül bir hatadır.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--log-level \u0026lt;level\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ewarn\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGünlük ayrıntı düzeyi: \u003ccode\u003edebug\u003c/code\u003e, \u003ccode\u003einfo\u003c/code\u003e, \u003ccode\u003ewarn\u003c/code\u003e veya \u003ccode\u003eerror\u003c/code\u003e. Günlük çıktısı stderr'e gider ve tarama sonuçlarını etkilemez.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"leakwatch-version\"\u003e\u003ccode\u003eleakwatch version\u003c/code\u003e\u003c/h2\u003e\n\u003cp\u003eİkili dosya sürümünü, commit karmasını ve derleme zaman damgasını yazdırır, ardından çıkar.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch version\n\u003c/code\u003e\u003c/pre\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eleakwatch v1.5.0 (commit: a3f9c12, built: 2026-05-10T08:22:00Z)\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"leakwatch-init\"\u003e\u003ccode\u003eleakwatch init\u003c/code\u003e\u003c/h2\u003e\n\u003cp\u003eGeçerli dizinde önerilen varsayılanlarla bir \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e yapılandırma dosyası oluşturur.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch init [flags]\n\u003c/code\u003e\u003c/pre\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output \u0026lt;path\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e.leakwatch.yaml\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYapılandırma dosyasını varsayılan yerine bu yola yaz.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--force\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMevcut bir yapılandırma dosyasının üzerine yaz. Bu bayrak olmadan, çıktı dosyası zaten mevcutsa \u003ccode\u003einit\u003c/code\u003e hatayla çıkar.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Varsayılan yapılandırmayı oluştur\nleakwatch init\n\n# Mevcut yapılandırmanın üzerine yaz\nleakwatch init --force\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"leakwatch-scan\"\u003e\u003ccode\u003eleakwatch scan\u003c/code\u003e\u003c/h2\u003e\n\u003cp\u003eTüm tarama alt komutları için üst komut. Kendi başına davranışı yoktur; bir alt komut çalıştırın.\u003c/p\u003e\n\u003ch3 id=\"ortak-tarama-bayraklar\"\u003eOrtak tarama bayrakları\u003c/h3\u003e\n\u003cp\u003eAşağıdaki bayraklar \u003cstrong\u003etüm\u003c/strong\u003e \u003ccode\u003escan\u003c/code\u003e alt komutlarında kullanılabilir.\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eKısa\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-f\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ejson\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktı biçimi: \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e veya \u003ccode\u003egithub\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-o\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estdout\u003c/td\u003e\n\u003ctd\u003eSonuçları stdout yerine bu dosya yoluna yaz. Uzantısız düz bir yol verildiğinde, biçimin kendi uzantısı otomatik olarak eklenir (örn. \u003ccode\u003e--format sarif --output results\u003c/code\u003e, \u003ccode\u003eresults.sarif\u003c/code\u003e dosyasını yazar). Dosya \u003ccode\u003e0600\u003c/code\u003e izinleriyle yazılır. Her zaman stdout'a yazan \u003ccode\u003e--format github\u003c/code\u003e için yok sayılır.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-c\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCPU sayısı\u003c/td\u003e\n\u003ctd\u003eEşzamanlı tarama çalışanı sayısı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--max-file-size\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e10485760\u003c/code\u003e (10 MB)\u003c/td\u003e\n\u003ctd\u003eBu bayt sayısından büyük dosyaları veya blob'ları atla.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--show-raw\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktıya ham (maskelenmemiş) sır değerini dahil et. Dikkatli kullanın.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-detectors\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eBu çalıştırma için hariç tutulacak dedektör kimlikleri (örn. \u003ccode\u003eaws-access-key-id,generic-api-key\u003c/code\u003e). Tekrarlanabilir veya virgülle ayrılabilir; yapılandırma dosyasındaki \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e ile birleştirilir (onun yerine geçmez).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--no-verify\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCanlı sır doğrulamasını devre dışı bırak. Giden API çağrısı yapılmaz.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYalnızca canlı doğrulama ile etkin olduğu teyit edilen bulguları raporla.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktıya dahil edilecek minimum önem derecesi: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e veya \u003ccode\u003ecritical\u003c/code\u003e. Tanınmayan bir değer (örn. bir yazım hatası) sessizce kabul edilmek yerine kesin bir hataya yol açar.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--remediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHer bulguya düzeltme rehberi (dönüşüm/iptal adımları) ekle.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e (tekrarlanabilir) bayrağı, \u003ccode\u003escan slack\u003c/code\u003e \u003cstrong\u003ehariç\u003c/strong\u003e her tarama alt komutunda da kullanılabilir — bu bayrak tüm alt komutlar arasında paylaşılmak yerine komut başına kaydedildiğinden, aşağıda her alt komutun kendi bayrak tablosuna bakın.\u003c/p\u003e\n\u003cp\u003eÇıkış kodu sözleşmesinin tamamı için \u003ca href=\"#/reference/exit-codes\"\u003eÇıkış Kodları\u003c/a\u003e sayfasına bakın: \u003ccode\u003e0\u003c/code\u003e temiz, \u003ccode\u003e1\u003c/code\u003e bulgu var, \u003ccode\u003e2\u003c/code\u003e hata, \u003ccode\u003e3\u003c/code\u003e tamamlanmadan kesildi.\u003c/p\u003e\n\u003chr\u003e\n\u003ch3 id=\"scan-fs\"\u003e\u003ccode\u003escan fs\u003c/code\u003e\u003c/h3\u003e\n\u003cp\u003eBir veya daha fazla yerel dosya sistemi yolunu tarar — her biri bir dizin (özyinelemeli olarak dolaşılır) veya tek bir dosya olabilir.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs [path...] [flags]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003cstrong\u003eSıfır veya daha fazla\u003c/strong\u003e konumsal argüman kabul eder. Yol verilmediğinde \u003ccode\u003e.\u003c/code\u003e (geçerli dizin) taranır. Tek bir çağrıda dosya ve dizinler karıştırılarak birden fazla yol verilebilir.\u003c/p\u003e\n\u003ch4 id=\"dosya-sistemine-zg-bayraklar\"\u003eDosya sistemine özgü bayraklar\u003c/h4\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eDışlanacak yollar için glob kalıbı. Tekrarlanabilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch4 id=\"rnekler\"\u003eÖrnekler\u003c/h4\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Geçerli dizini tara, renklendirilmiş tablo yazdır\nleakwatch scan fs . --format table\n\n# Tek bir dosyayı tara\nleakwatch scan fs cmd/main.go\n\n# Birden fazla dosya ve dizini birlikte tara\nleakwatch scan fs cmd/ main.go internal/config\n\n# SARIF çıktısını kaydet, test dosyalarını ve vendor'ı dışla\nleakwatch scan fs . \\\n --exclude \u0026quot;**/*_test.go\u0026quot; \\\n --exclude \u0026quot;vendor/**\u0026quot; \\\n --format sarif \\\n --output results.sarif\n\n# Bu çalıştırma için belirli dedektörleri hariç tut\nleakwatch scan fs . --exclude-detectors aws-access-key-id,generic-api-key\n\u003c/code\u003e\u003c/pre\u003e\n\u003chr\u003e\n\u003ch3 id=\"scan-git\"\u003e\u003ccode\u003escan git\u003c/code\u003e\u003c/h3\u003e\n\u003cp\u003eYerel veya uzak bir Git deposunun tam commit geçmişini tarar.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git \u0026lt;url_or_path\u0026gt; [flags]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eTam olarak bir konumsal argüman gereklidir: yerel bir yol veya HTTP/HTTPS/SSH URL'si.\u003c/p\u003e\n\u003ch4 id=\"gite-zg-bayraklar\"\u003eGit'e özgü bayraklar\u003c/h4\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--since \u0026lt;YYYY-MM-DD\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eYalnızca bu tarihten sonraki commit'leri tara.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--since-commit \u0026lt;hash\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eYalnızca bu commit karmasından HEAD'e kadar olan değişiklikleri tara.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--branch \u0026lt;name\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eVarsayılan dal yerine belirli bir dalı hedefle.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--depth \u0026lt;int\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e (tam)\u003c/td\u003e\n\u003ctd\u003eUzak depolar için sığ klonlama derinliği. \u003ccode\u003e0\u003c/code\u003e tam geçmişi getirir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eDışlanacak blob yolları için glob kalıbı. Tekrarlanabilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch4 id=\"rnekler-1\"\u003eÖrnekler\u003c/h4\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Tam yerel geçmişi tara\nleakwatch scan git . --format table\n\n# Bir pull request tarafından eklenen commit'leri tara\nleakwatch scan git . --since-commit a1b2c3d --format json\n\u003c/code\u003e\u003c/pre\u003e\n\u003chr\u003e\n\u003ch3 id=\"scan-image\"\u003e\u003ccode\u003escan image\u003c/code\u003e\u003c/h3\u003e\n\u003cp\u003eBir OCI/Docker imajının katmanlarını sırlar açısından tarar. Leakwatch daemonsuz çalışır ve kayıt defterinden doğrudan çeker — Docker soketi gerekmez.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan image \u0026lt;image:tag\u0026gt; [flags]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eTam olarak bir konumsal argüman gereklidir. Ortak tarama bayraklarının ötesinde imaja özgü bir bayrak yoktur; ortak bayraklar arasında, katmanlar içindeki dosya yollarıyla eşleştirilen \u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e (tekrarlanabilir) de bulunur.\u003c/p\u003e\n\u003ch4 id=\"rnekler-2\"\u003eÖrnekler\u003c/h4\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Genel bir imajı tara\nleakwatch scan image nginx:latest --format table\n\n# Özel kayıt defteri imajını tara, JSON çıktısını kaydet\nleakwatch scan image registry.example.com/my-app:v2.3.0 \\\n --format json \\\n --output image-results.json\n\u003c/code\u003e\u003c/pre\u003e\n\u003chr\u003e\n\u003ch3 id=\"scan-s3\"\u003e\u003ccode\u003escan s3\u003c/code\u003e\u003c/h3\u003e\n\u003cp\u003eBir AWS S3 kovasındaki nesneleri tarar.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan s3 \u0026lt;bucket\u0026gt; [flags]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eTam olarak bir konumsal argüman gereklidir.\u003c/p\u003e\n\u003ch4 id=\"s3e-zg-bayraklar\"\u003eS3'e özgü bayraklar\u003c/h4\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--prefix \u0026lt;string\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eTaramayı, anahtarı bu ön ekle başlayan nesnelerle sınırla.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--region \u0026lt;string\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eKovanın bulunduğu AWS bölgesi. \u003ccode\u003eAWS_REGION\u003c/code\u003e ortam değişkenine veya AWS SDK varsayılanına geri döner.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eDışlanacak nesne anahtarları için glob kalıbı. Tekrarlanabilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch4 id=\"rnekler-3\"\u003eÖrnekler\u003c/h4\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Tüm kovayı tara\nleakwatch scan s3 my-data-bucket --region us-east-1 --format table\n\n# Belirli bir ön eki tara\nleakwatch scan s3 my-data-bucket --prefix backups/2026/ --format json\n\u003c/code\u003e\u003c/pre\u003e\n\u003chr\u003e\n\u003ch3 id=\"scan-gcs\"\u003e\u003ccode\u003escan gcs\u003c/code\u003e\u003c/h3\u003e\n\u003cp\u003eBir Google Cloud Storage kovasındaki nesneleri tarar.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan gcs \u0026lt;bucket\u0026gt; [flags]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eTam olarak bir konumsal argüman gereklidir.\u003c/p\u003e\n\u003ch4 id=\"gcsye-zg-bayraklar\"\u003eGCS'ye özgü bayraklar\u003c/h4\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--prefix \u0026lt;string\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eTaramayı, adı bu ön ekle başlayan nesnelerle sınırla.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--project \u0026lt;string\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eGCP proje kimliği. Varsayılan kimlik bilgilerinden proje çıkarılamadığında gereklidir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eDışlanacak nesne adları için glob kalıbı. Tekrarlanabilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch4 id=\"rnekler-4\"\u003eÖrnekler\u003c/h4\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Tüm GCS kovasını tara\nleakwatch scan gcs my-gcs-bucket --project my-gcp-project --format table\n\n# Ön ek tara\nleakwatch scan gcs my-gcs-bucket --prefix uploads/2026/ --format json\n\u003c/code\u003e\u003c/pre\u003e\n\u003chr\u003e\n\u003ch3 id=\"scan-slack\"\u003e\u003ccode\u003escan slack\u003c/code\u003e\u003c/h3\u003e\n\u003cp\u003eBir Slack çalışma alanındaki mesaj metnini tarar.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan slack [flags]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eKonumsal argüman yoktur.\u003c/p\u003e\n\u003ch4 id=\"slacke-zg-bayraklar\"\u003eSlack'e özgü bayraklar\u003c/h4\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--token \u0026lt;string\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eSlack bot token'ı. \u003ccode\u003eLEAKWATCH_SLACK_TOKEN\u003c/code\u003e ortam değişkeni ile de ayarlanabilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--channels \u0026lt;list\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eTaranacak kanal adları veya kimliklerinin virgülle ayrılmış listesi. Atlandığında erişilebilir tüm kanalları tarar.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-channels \u0026lt;list\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eAtlanacak kanal adları veya kimliklerinin virgülle ayrılmış listesi.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--since \u0026lt;YYYY-MM-DD\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eYalnızca bu tarihten sonra gönderilen mesajları tara.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--include-dms\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDoğrudan mesajları dahil et (ek OAuth kapsamları gerektirir).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--rate-limit \u0026lt;float\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSaniye başına maksimum Slack API isteği.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003ccode\u003escan slack\u003c/code\u003e komutunda (diğer her tarama alt komutunun aksine) \u003ccode\u003e--exclude\u003c/code\u003e yol-kalıbı bayrağı yoktur — bunun yerine tüm kanalları atlamak için \u003ccode\u003e--exclude-channels\u003c/code\u003e kullanın.\u003c/p\u003e\n\u003ch4 id=\"rnekler-5\"\u003eÖrnekler\u003c/h4\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Erişilebilir tüm kanalları tara\nleakwatch scan slack --token xoxb-••••••••••••-••••••••••••-•••••••••••••••••••••••• --format table\n\n# Belirli kanalları belirli bir tarihten itibaren tara\nleakwatch scan slack \\\n --token xoxb-••••••••••••-••••••••••••-••••••••••••••••••••••••• \\\n --channels general,engineering \\\n --since 2026-01-01 \\\n --format json\n\u003c/code\u003e\u003c/pre\u003e\n\u003chr\u003e\n\u003ch3 id=\"scan-repos\"\u003e\u003ccode\u003escan repos\u003c/code\u003e\u003c/h3\u003e\n\u003cp\u003eBirden fazla Git deposunu paralel olarak tarar.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan repos \u0026lt;url_or_path...\u0026gt; [flags]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eEn az iki konumsal argüman (depo URL'leri veya yerel yollar) gereklidir.\u003c/p\u003e\n\u003ch4 id=\"reposa-zg-bayraklar\"\u003eRepos'a özgü bayraklar\u003c/h4\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eKısa\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--parallel\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eEşzamanlı olarak taranacak depo sayısı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-c\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCPU sayısı\u003c/td\u003e\n\u003ctd\u003eHer depo taramasındaki çalışan eşzamanlılığı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eHer depoya uygulanan, dışlanacak blob yolları için glob kalıbı. Tekrarlanabilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch4 id=\"rnekler-6\"\u003eÖrnekler\u003c/h4\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# İki depoyu paralel olarak tara\nleakwatch scan repos \\\n https://github.com/org/repo-a.git \\\n https://github.com/org/repo-b.git \\\n --format json\n\n# Büyük bir depo seti için paralellizmi artır\nleakwatch scan repos \\\n https://github.com/org/repo-a.git \\\n https://github.com/org/repo-b.git \\\n https://github.com/org/repo-c.git \\\n --parallel 3 \\\n --format sarif \\\n --output multi-repo.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003chr\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/reference/exit-codes\"\u003eÇıkış Kodları\u003c/a\u003e — çıkış kodlarının tarama sonuçlarıyla nasıl eşleştiği.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/environment-variables\"\u003eOrtam Değişkenleri\u003c/a\u003e — Leakwatch'ı bayrak kullanmadan yapılandırma.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/filesystem\"\u003eDosya Sistemi Taraması\u003c/a\u003e — ayrıntılı \u003ccode\u003escan fs\u003c/code\u003e rehberi.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/git-history\"\u003eGit Geçmişi\u003c/a\u003e — ayrıntılı \u003ccode\u003escan git\u003c/code\u003e rehberi.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e — \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e başvurusu.\u003c/li\u003e\n\u003c/ul\u003e\n"},"reference/environment-variables":{"title":"Ortam Değişkenleri","description":"Leakwatch davranışını bayrak kullanmadan yapılandıran ortam değişkenleri.","html":"\u003ch1 id=\"ortam-deikenleri\"\u003eOrtam Değişkenleri\u003c/h1\u003e\n\u003cp\u003eLeakwatch, yapılandırmayı öncelik sırasına göre üç kaynaktan okur: \u003cstrong\u003ekomut satırı bayrakları\u003c/strong\u003e, \u003cstrong\u003eortam değişkenlerini\u003c/strong\u003e geçersiz kılar; ortam değişkenleri \u003cstrong\u003eyapılandırma dosyasını\u003c/strong\u003e (\u003ccode\u003e.leakwatch.yaml\u003c/code\u003e) geçersiz kılar; yapılandırma dosyası yerleşik \u003cstrong\u003evarsayılanlara\u003c/strong\u003e geri döner. Ortam değişkenleri, bir yapılandırma dosyasını değiştiremeyeceğiniz veya her çağrıya bayrak geçiremeyeceğiniz CI ortamlarında kullanışlıdır.\u003c/p\u003e\n\u003ch2 id=\"yaplandrma-deikeni-kalb\"\u003eYapılandırma değişkeni kalıbı\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003e.leakwatch.yaml\u003c/code\u003e'daki herhangi bir anahtar, ortam değişkeni olarak şu şekilde ayarlanabilir:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eAnahtar adını büyük harfe çevir.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e.\u003c/code\u003e ve \u003ccode\u003e-\u003c/code\u003e karakterlerini \u003ccode\u003e_\u003c/code\u003e ile değiştir.\u003c/li\u003e\n\u003cli\u003eBaşına \u003ccode\u003eLEAKWATCH_\u003c/code\u003e ekle.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eÖrneğin, \u003ccode\u003escan.concurrency\u003c/code\u003e yapılandırma anahtarı \u003ccode\u003eLEAKWATCH_SCAN_CONCURRENCY\u003c/code\u003e olur.\u003c/p\u003e\n\u003ch2 id=\"deiken-bavurusu\"\u003eDeğişken başvurusu\u003c/h2\u003e\n\u003ch3 id=\"leakwatcha-zg-deikenler\"\u003eLeakwatch'a özgü değişkenler\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eDeğişken\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_SLACK_TOKEN\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003escan slack\u003c/code\u003e için Slack bot token'ı. \u003ccode\u003e--token\u003c/code\u003e'a eşdeğer. Token'ın kabuk geçmişinde veya CI günlüklerinde görünmesini önlemek için bayrak olarak geçirmek yerine bunu ayarlayın.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_SCAN_CONCURRENCY\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eEşzamanlı tarama çalışanı sayısı. \u003ccode\u003e--concurrency\u003c/code\u003e'e eşdeğer.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_VERIFICATION_ENABLED\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCanlı doğrulamayı genel olarak devre dışı bırakmak için \u003ccode\u003efalse\u003c/code\u003e olarak ayarlayın. \u003ccode\u003e--no-verify\u003c/code\u003e'e eşdeğer.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_VERIFICATION_RATE_LIMIT\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTüm doğrulayıcılar genelinde saniye başına maksimum doğrulama isteği.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_OUTPUT_FORMAT\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eVarsayılan çıktı biçimi (\u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e veya \u003ccode\u003egithub\u003c/code\u003e). \u003ccode\u003e--format\u003c/code\u003e'a eşdeğer.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_DETECTION_ENTROPY_THRESHOLD\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBir eşleşmenin raporlanması için gereken minimum Shannon entropisi. Float değer, örn. \u003ccode\u003e3.5\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"grntleme-deikeni\"\u003eGörüntüleme değişkeni\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eDeğişken\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eNO_COLOR\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBoş olmayan herhangi bir değere ayarlandığında, \u003ccode\u003etable\u003c/code\u003e çıktı biçimlendiricisindeki ANSI renk kodlarını devre dışı bırakır. \u003ca href=\"https://no-color.org\"\u003eno-color.org\u003c/a\u003e kuralını izler.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"aws-deikenleri-scan-s3-ve-aws-sr-dorulamas-iin\"\u003eAWS değişkenleri (\u003ccode\u003escan s3\u003c/code\u003e ve AWS sır doğrulaması için)\u003c/h3\u003e\n\u003cp\u003eBunlar standart AWS SDK ortam değişkenleridir. Leakwatch bunları AWS SDK v2 varsayılan kimlik bilgisi zincirine aktarır.\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eDeğişken\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eAWS_ACCESS_KEY_ID\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAWS erişim anahtarı kimliği.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eAWS_SECRET_ACCESS_KEY\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAWS gizli erişim anahtarı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eAWS_SESSION_TOKEN\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAWS oturum token'ı (geçici kimlik bilgileri için).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eAWS_REGION\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eVarsayılan AWS bölgesi.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eAWS_PROFILE\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eKullanılacak \u003ccode\u003e~/.aws/credentials\u003c/code\u003e dosyasından adlandırılmış profil.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"gcs-deikeni-scan-gcs-iin\"\u003eGCS değişkeni (\u003ccode\u003escan gcs\u003c/code\u003e için)\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eDeğişken\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eGOOGLE_APPLICATION_CREDENTIALS\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGoogle hizmet hesabı JSON anahtar dosyasının yolu. Bir GCS kovasını tararken Uygulama Varsayılan Kimlik Bilgileri tarafından kullanılır.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"ncelik-rnei\"\u003eÖncelik örneği\u003c/h2\u003e\n\u003cp\u003eŞu kurulumu varsayın:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e.leakwatch.yaml\u003c/code\u003e, \u003ccode\u003eoutput.format: table\u003c/code\u003e olarak ayarlıyor\u003c/li\u003e\n\u003cli\u003eOrtamda \u003ccode\u003eLEAKWATCH_OUTPUT_FORMAT=json\u003c/code\u003e ayarlanmış\u003c/li\u003e\n\u003cli\u003eKomut \u003ccode\u003eleakwatch scan fs .\u003c/code\u003e olarak çalıştırılıyor (\u003ccode\u003e--format\u003c/code\u003e bayrağı yok)\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eOrtam değişkeni yapılandırma dosyasını geçersiz kıldığından geçerli biçim \u003ccode\u003ejson\u003c/code\u003e'dır.\u003c/p\u003e\n\u003cp\u003eKomut \u003ccode\u003eleakwatch scan fs . --format sarif\u003c/code\u003e olarak çalıştırılırsa, bayrak her şeyi geçersiz kıldığından geçerli biçim \u003ccode\u003esarif\u003c/code\u003e olur.\u003c/p\u003e\n\u003ch2 id=\"dorulama-kimlik-bilgileri-ve-tarama-kimlik-bilgileri\"\u003eDoğrulama kimlik bilgileri ve tarama kimlik bilgileri\u003c/h2\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNot\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eYukarıdaki AWS ve GCP değişkenleri, Leakwatch'ın \u003cstrong\u003ekendisinin\u003c/strong\u003e nesneleri taramak için S3 veya GCS'ye bağlanırken kimliğini doğrulaması için kullanılır. Bulunan sırları doğrulamak için kullanılmazlar. Keşfedilen bir AWS anahtarının doğrulanması, örneğin, runner'ın kimlik bilgilerini değil, keşfedilen anahtarın kendisini kullanarak AWS STS'yi çağırır.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"cida-srlar-gvenli-biimde-geirme\"\u003eCI'da sırları güvenli biçimde geçirme\u003c/h2\u003e\n\u003cp\u003eGitHub Actions'ta şifrelenmiş sırları kullanın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003eenv:\n LEAKWATCH_SLACK_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eGitLab CI'da maskelenmiş CI/CD değişkenlerini kullanın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003evariables:\n LEAKWATCH_SLACK_TOKEN: $SLACK_BOT_TOKEN # proje ayarlarında maskelenmiş değişken olarak tanımlanmış\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eToken değerlerini hiçbir zaman iş akışı dosyalarına veya Dockerfile'lara sabit olarak kodlamayın.\u003c/p\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e — tam \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e anahtar başvurusu.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/cloud-storage\"\u003eBulut Depolama Taraması\u003c/a\u003e — \u003ccode\u003escan s3\u003c/code\u003e ve \u003ccode\u003escan gcs\u003c/code\u003e kimlik bilgileri.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/slack\"\u003eSlack Taraması\u003c/a\u003e — Slack token kapsamları ve izinleri.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Başvurusu\u003c/a\u003e — eşdeğer komut satırı bayrakları.\u003c/li\u003e\n\u003c/ul\u003e\n"},"reference/exit-codes":{"title":"Çıkış Kodları","description":"Leakwatch çıkış kodu başvurusu ve bunların betiklerde ve CI pipeline'larında nasıl kullanılacağı.","html":"\u003ch1 id=\"k-kodlar\"\u003eÇıkış Kodları\u003c/h1\u003e\n\u003cp\u003eLeakwatch, CI pipeline'larının ve kabuk betiklerinin çıktıyı ayrıştırmadan tarama sonuçlarına göre hareket edebilmesi için küçük, iyi tanımlanmış bir çıkış kodu seti kullanır. Her tarama alt komutu dört koddan biriyle çıkar.\u003c/p\u003e\n\u003ch2 id=\"kod-bavurusu\"\u003eKod başvurusu\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eKod\u003c/th\u003e\n\u003cth\u003eAd\u003c/th\u003e\n\u003cth\u003eAnlam\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTemiz\u003c/td\u003e\n\u003ctd\u003eTarama başarıyla tamamlandı ve etkin filtrelerden hiçbir bulgu geçmedi.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBulgular var\u003c/td\u003e\n\u003ctd\u003eTarama tamamlandı ve etkin filtrelerden geçen bir veya daha fazla sır bulundu (ve etkin filtrelerden geçti). Bu, diğer her sonuca göre önceliklidir: bulgular raporlanmışsa, tarama daha sonra kesintiye uğrasa veya çoklu depo taramasındaki bir depo başarısız olsa bile çıkış kodu \u003ccode\u003e1\u003c/code\u003e döndürülür.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHata\u003c/td\u003e\n\u003ctd\u003eTaramanın hiç çalışamamasına neden olan ciddi bir hata oluştu — örneğin geçersiz bir bayrak, okunamaz bir yol veya kimlik doğrulama hatası. Stderr'e bir \u003ccode\u003eError: ...\u003c/code\u003e mesajı ve kullanım ipucu yazdırılır.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eKesintiye uğradı\u003c/td\u003e\n\u003ctd\u003eTarama tamamlanmadan önce \u003ccode\u003eSIGINT\u003c/code\u003e (\u003ccode\u003eCtrl+C\u003c/code\u003e) veya \u003ccode\u003eSIGTERM\u003c/code\u003e aldı ve kesinti anında \u003cstrong\u003ehiçbir\u003c/strong\u003e bulgu raporlamamıştı. Kısmi sonuçlar (filtrelemeden geçen varsa) yine de yapılandırılmış çıktıya yazılır. Bu nedenle bir CI hattı, tamamlanmadan biten bir tarama için asla temiz bir \u003ccode\u003e0\u003c/code\u003e çıkışı gözlemleyemez.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"filtrelerin-k-kodu-1i-nasl-etkiledii\"\u003eFiltrelerin çıkış kodu 1'i nasıl etkilediği\u003c/h2\u003e\n\u003cp\u003eÇıkış kodu \u003ccode\u003e1\u003c/code\u003e, yalnızca en az bir bulgu etkin çıktı filtrelerinin tümünden geçtiğinde yayılır. En ilgili iki filtre şunlardır:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/strong\u003e — eşiğin altındaki bulgular bastırılır. Tüm bulgular \u003ccode\u003elow\u003c/code\u003e önem derecesindeyse ve \u003ccode\u003e--min-severity high\u003c/code\u003e ile çalışıyorsanız, sırlar mevcut olmasına rağmen çıkış kodu \u003ccode\u003e0\u003c/code\u003e döndürülür.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/strong\u003e — yalnızca canlı doğrulama ile etkin olduğu teyit edilen bulgular raporlanır. Etkin sır bulunamazsa çıkış kodu \u003ccode\u003e0\u003c/code\u003e döndürülür.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eBu, çıkış kodu \u003ccode\u003e0\u003c/code\u003e'ın \u0026quot;mevcut filtre ayarlarınızla eşleşen bulgu yok\u0026quot; anlamına geldiği anlamına gelir — kod tabanının hiçbir sır içermediği değil.\u003c/p\u003e\n\u003cdiv class=\"callout callout-warn\"\u003e\u003cdiv class=\"callout-label\"\u003eUyarı\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003e\u003ccode\u003e--only-verified\u003c/code\u003e altında temiz \u003ccode\u003e0\u003c/code\u003e çıkışı, kod tabanının sırdan arındırılmış olduğunu garanti etmez. Doğrulamanın mevcut olmadığı sır türleri (10 dedektör türü) her zaman doğrulanmamış olarak raporlanır ve \u003ccode\u003e--only-verified\u003c/code\u003e tarafından bastırılır. Tam kapsam için \u003ccode\u003e--only-verified\u003c/code\u003e ile birlikte ayrı bir filtresiz tarama yapın.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"kabuk-betiklerinde-k-kodlarn-kullanma\"\u003eKabuk betiklerinde çıkış kodlarını kullanma\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e#!/usr/bin/env bash\nset +e\nleakwatch scan fs . --format json --output leakwatch.json --no-verify\nEXIT_CODE=$?\nset -e\n\ncase \u0026quot;$EXIT_CODE\u0026quot; in\n 0)\n echo \u0026quot;Sır bulunamadı. Derleme devam ediyor.\u0026quot;\n ;;\n 1)\n echo \u0026quot;Sırlar bulundu — birleştirmeden önce leakwatch.json'u inceleyin ve düzeltin.\u0026quot;\n exit 1\n ;;\n 3)\n echo \u0026quot;Tarama tamamlanmadan kesintiye uğradı — güvenilmez sayın, birleştirmeyin.\u0026quot;\n exit 3\n ;;\n *)\n echo \u0026quot;Leakwatch bir hatayla karşılaştı (çıkış $EXIT_CODE).\u0026quot;\n exit \u0026quot;$EXIT_CODE\u0026quot;\n ;;\nesac\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eTaramadan önce \u003ccode\u003eset +e\u003c/code\u003e kullanmak, kabuğun sıfır dışı kodlarda çıkmasını engeller ve kodu kendiniz yakalayıp işlemenize olanak tanır.\u003c/p\u003e\n\u003ch2 id=\"ci-pipelinelarnda-k-kodlarn-kullanma\"\u003eCI pipeline'larında çıkış kodlarını kullanma\u003c/h2\u003e\n\u003cp\u003eÇoğu CI sistemi, sıfır dışı herhangi bir çıkış kodunu adım başarısızlığı olarak değerlendirir. Leakwatch sırlar bulunduğunda \u003ccode\u003e1\u003c/code\u003e ile çıktığından, ek yapılandırma olmadan pipeline otomatik olarak başarısız olur — yalnızca tarama komutunu çalıştırın.\u003c/p\u003e\n\u003cp\u003eSırlar bulunsa bile pipeline'ın devam etmesine izin vermek için (örneğin, derlemeyi engellemeden raporu toplamak amacıyla) çıkış kodunu açıkça yoksayın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --format sarif --output results.sarif --no-verify || true\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eYa da GitLab CI'da:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003eallow_failure: true\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eYa da GitHub Action'da \u003ccode\u003efail-on-findings: \u0026quot;false\u0026quot;\u003c/code\u003e olarak ayarlayın.\u003c/p\u003e\n\u003ch2 id=\"uygulamada-k-kodu-2\"\u003eUygulamada çıkış kodu 2\u003c/h2\u003e\n\u003cp\u003eÇıkış kodu \u003ccode\u003e2\u003c/code\u003e, taramanın hiç çalışamamasına neden olan bir yapılandırma veya çalışma zamanı hatasını gösterir. Yaygın nedenler:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eGeçersiz bir bayrak değeri (örneğin \u003ccode\u003e--format invalid\u003c/code\u003e).\u003c/li\u003e\n\u003cli\u003eMevcut olmayan veya okunamaz bir yol.\u003c/li\u003e\n\u003cli\u003eEksik gerekli argüman (örneğin, URL olmadan \u003ccode\u003escan git\u003c/code\u003e).\u003c/li\u003e\n\u003cli\u003eBir bulut kaynağına bağlanırken kimlik doğrulama hatası.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eHata mesajı stderr'e yazdırılır ve sorunu teşhis etmeye yardımcı olacak bağlam içerir. Örneğin, geçersiz bir \u003ccode\u003e--format\u003c/code\u003e değeri şunu üretir:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eError: failed to load configuration: unsupported output format: xlsx\n\nRun 'leakwatch scan fs --help' for usage information.\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eGeçerli \u003ccode\u003e--format\u003c/code\u003e değerleri \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e ve \u003ccode\u003egithub\u003c/code\u003e'dır.\u003c/p\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/other-ci\"\u003eDiğer CI Sistemleri\u003c/a\u003e — çıkış kodlarını GitLab CI, Jenkins ve diğerlerine bağlama.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/github-action\"\u003eGitHub Action\u003c/a\u003e — resmi action'ın çıkış kodlarını adım sonuçlarıyla nasıl eşlediği.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Başvurusu\u003c/a\u003e — tam bayrak başvurusu.\u003c/li\u003e\n\u003c/ul\u003e\n"},"scanning/cloud-storage":{"title":"Bulut Depolama (S3 \u0026 GCS)","description":"AWS S3 ve Google Cloud Storage kovalarını sızan sırlara karşı tarayın.","html":"\u003ch1 id=\"bulut-depolama-s3--gcs\"\u003eBulut Depolama (S3 \u0026amp; GCS)\u003c/h1\u003e\n\u003cp\u003eSırlar sıklıkla bulut depolamaya taşınır — dışa aktarılan veritabanı dökümleri, ortam dosyaları, CI artefaktları ve günlük arşivleri, düşünüldüğünden çok daha fazla kişinin erişebildiği kovalara akar. Leakwatch, AWS S3 ve Google Cloud Storage kovalarını nesne nesne tarayabilir ve bulduğu sırları bir olaya dönüşmeden işaretler.\u003c/p\u003e\n\u003ch2 id=\"aws-s3\"\u003eAWS S3\u003c/h2\u003e\n\u003ch3 id=\"kullanm\"\u003eKullanım\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan s3 \u0026lt;bucket\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eKomut tam olarak bir argüman alır: \u003cstrong\u003ekova adı\u003c/strong\u003e (\u003ccode\u003es3://\u003c/code\u003e öneki olmadan). Tarama hedefi \u003ccode\u003es3://\u0026lt;bucket\u0026gt;\u003c/code\u003e olarak gösterilir.\u003c/p\u003e\n\u003ch3 id=\"kimlik-dorulama\"\u003eKimlik doğrulama\u003c/h3\u003e\n\u003cp\u003eLeakwatch standart \u003ca href=\"https://docs.aws.amazon.com/sdkref/latest/guide/standardized-credentials.html\"\u003eAWS varsayılan kimlik bilgisi zincirini\u003c/a\u003e kullanır:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eOrtam değişkenleri (\u003ccode\u003eAWS_ACCESS_KEY_ID\u003c/code\u003e, \u003ccode\u003eAWS_SECRET_ACCESS_KEY\u003c/code\u003e, \u003ccode\u003eAWS_SESSION_TOKEN\u003c/code\u003e).\u003c/li\u003e\n\u003cli\u003ePaylaşılan kimlik bilgileri dosyası (\u003ccode\u003e~/.aws/credentials\u003c/code\u003e).\u003c/li\u003e\n\u003cli\u003ePaylaşılan yapılandırma dosyası (\u003ccode\u003e~/.aws/config\u003c/code\u003e).\u003c/li\u003e\n\u003cli\u003eÖrneğe veya göreve atanmış IAM rolü (EC2, ECS, Lambda).\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eAWS CLI ile zaten kimlik doğrulaması yaptıysanız (\u003ccode\u003eaws configure\u003c/code\u003e veya üstlenilmiş bir rol) ek yapılandırma gerekmez.\u003c/p\u003e\n\u003ch3 id=\"s3e-zg-bayraklar\"\u003eS3'e özgü bayraklar\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eTür\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--prefix\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eYalnızca anahtarı bu önekle başlayan nesneleri tara.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--region\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003eAWS yapılandırmasından\u003c/td\u003e\n\u003ctd\u003eKovanın bulunduğu AWS bölgesi.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"s3-rnekleri\"\u003eS3 örnekleri\u003c/h3\u003e\n\u003cp\u003eTüm kovayı tarayın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan s3 my-config-bucket\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBelirli bir bölgede belirli bir anahtar öneki altındaki nesneleri tarayın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan s3 my-bucket --prefix logs/ --region us-east-1\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eSARIF olarak kaydedin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan s3 my-bucket --format sarif --output s3-results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eİpucu\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eTaramayı ilgili bir alt yola sınırlamak için \u003ccode\u003e--prefix\u003c/code\u003e kullanın. Milyonlarca nesne içeren büyük bir kovayı taramak yavaş olabilir ve S3 GET istek maliyeti doğurabilir. Öneki gerçekten önemli olana — örneğin \u003ccode\u003econfigs/\u003c/code\u003e veya \u003ccode\u003eexports/\u003c/code\u003e — daraltın.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003chr\u003e\n\u003ch2 id=\"google-cloud-storage\"\u003eGoogle Cloud Storage\u003c/h2\u003e\n\u003ch3 id=\"kullanm-1\"\u003eKullanım\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan gcs \u0026lt;bucket\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eKomut tam olarak bir argüman alır: \u003cstrong\u003ekova adı\u003c/strong\u003e (\u003ccode\u003egs://\u003c/code\u003e öneki olmadan). Tarama hedefi \u003ccode\u003egs://\u0026lt;bucket\u0026gt;\u003c/code\u003e olarak gösterilir.\u003c/p\u003e\n\u003ch3 id=\"kimlik-dorulama-1\"\u003eKimlik doğrulama\u003c/h3\u003e\n\u003cp\u003eLeakwatch \u003ca href=\"https://cloud.google.com/docs/authentication/application-default-credentials\"\u003eApplication Default Credentials (ADC)\u003c/a\u003e kullanır. Kimlik bilgisi arama sırası şu şekildedir:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eHizmet hesabı anahtar dosyasına işaret eden \u003ccode\u003eGOOGLE_APPLICATION_CREDENTIALS\u003c/code\u003e ortam değişkeni.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003egcloud auth application-default login\u003c/code\u003e ile yapılandırılmış kullanıcı kimlik bilgileri.\u003c/li\u003e\n\u003cli\u003eGoogle Compute Engine örneğine, Cloud Run hizmetine veya GKE iş yüküne atanmış hizmet hesabı.\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"gcsye-zg-bayraklar\"\u003eGCS'ye özgü bayraklar\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eTür\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--prefix\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eYalnızca adı bu önekle başlayan nesneleri tara.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--project\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eGCP proje kimliği (bazı ADC yapılandırmalarında gereklidir).\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"gcs-rnekleri\"\u003eGCS örnekleri\u003c/h3\u003e\n\u003cp\u003eBelirli bir GCP projesiyle tüm kovayı tarayın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan gcs my-config-bucket --project my-gcp-project\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eYalnızca belirli bir önek altındaki nesneleri tarayın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan gcs my-bucket --project my-gcp-project --prefix exports/\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eCSV olarak çıktı alın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan gcs my-bucket --format csv --output gcs-results.csv\n\u003c/code\u003e\u003c/pre\u003e\n\u003chr\u003e\n\u003ch2 id=\"ortak-tarama-bayraklar\"\u003eOrtak tarama bayrakları\u003c/h2\u003e\n\u003cp\u003eHem \u003ccode\u003es3\u003c/code\u003e hem de \u003ccode\u003egcs\u003c/code\u003e aynı ortak tarama bayraklarını destekler:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eKısa\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-f\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ejson\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktı biçimi: \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, \u003ccode\u003egithub\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-o\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estdout\u003c/td\u003e\n\u003ctd\u003eSonuçları stdout yerine bu dosyaya yaz.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-c\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCPU sayısı\u003c/td\u003e\n\u003ctd\u003eEşzamanlı çalışan sayısı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--max-file-size\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e10485760\u003c/code\u003e (10 MB)\u003c/td\u003e\n\u003ctd\u003eBu boyutu aşan nesneleri atla (bayt).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--show-raw\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktıda ham sır değerini göster.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eDışlanacak nesne anahtarı kalıbı. Tekrarlanabilir; \u003ccode\u003efilter.exclude-paths\u003c/code\u003e ile birleştirilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-detectors\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eBu çalıştırma için hariç tutulacak dedektör kimlikleri. Tekrarlanabilir; \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e ile birleştirilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--no-verify\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSır doğrulamasını devre dışı bırak.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYalnızca doğrulama ile aktif olduğu onaylanan bulguları raporla.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRaporlanacak minimum önem: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e, \u003ccode\u003ecritical\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--remediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHer bulguya düzeltme rehberi ekle.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eNesne anahtarlarına uygulanan yol dışlamaları \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e dosyasında \u003ccode\u003efilter.exclude-paths\u003c/code\u003e altında ya da her çalıştırma için \u003ccode\u003e--exclude\u003c/code\u003e ile yapılandırılır. \u003ccode\u003e--config\u003c/code\u003e ve \u003ccode\u003e--log-level\u003c/code\u003e (varsayılan \u003ccode\u003ewarn\u003c/code\u003e) kök bayrakları da geçerlidir.\u003c/p\u003e\n\u003ch2 id=\"bulgu-meta-verisi\"\u003eBulgu meta verisi\u003c/h2\u003e\n\u003cp\u003eBir S3 veya GCS bulgusu yalnızca her kaynakta ortak olan alanları ve \u003ccode\u003e\u0026lt;bucket\u0026gt;/\u0026lt;object-key\u0026gt;\u003c/code\u003e olarak ayarlanan \u003ccode\u003efile_path\u003c/code\u003e alanını taşır (bulut depolama bulguları için ayrı bir \u003ccode\u003eline\u003c/code\u003e alanı yoktur).\u003c/p\u003e\n\u003ch2 id=\"k-kodlar\"\u003eÇıkış kodları\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eKod\u003c/th\u003e\n\u003cth\u003eAnlam\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlandı, bulgu yok.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlandı, bulgular raporlandı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama başarısız oldu (kimlik doğrulama hatası, kova bulunamadı, vb.).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlanmadan kesildi (\u003ccode\u003eCtrl+C\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e) ve hiçbir bulgu raporlanmamıştı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eHer çalıştırmanın ardından stderr'e bir tarama özeti yazdırılır. Taramalar SIGINT/SIGTERM sinyalinde düzgün biçimde iptal edilir.\u003c/p\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eHızlı Başlangıç\u003c/a\u003e — ilk taramanızı bir dakikadan kısa sürede çalıştırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e — dışlamaları ve diğer varsayılanları yapılandırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/ignoring-findings\"\u003eBulguları Yoksayma\u003c/a\u003e — bilinen yanlış pozitifleri bastırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eDoğrulama Nasıl Çalışır\u003c/a\u003e — doğrulama durumlarını anlayın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/filesystem\"\u003eDosya Sistemi\u003c/a\u003e — yerel bir dizin ağacını tarayın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Referansı\u003c/a\u003e — tüm komutlar için tam bayrak referansı.\u003c/li\u003e\n\u003c/ul\u003e\n"},"scanning/container-images":{"title":"Konteyner İmajları","description":"Docker daemon gerektirmeksizin OCI ve Docker imaj katmanlarını sızan sırlara karşı tarayın.","html":"\u003ch1 id=\"konteyner-majlar\"\u003eKonteyner İmajları\u003c/h1\u003e\n\u003cp\u003eKonteyner imajları sırların sıklıkla gizlendiği yerlerden biridir: ortam değişkenlerine gömülen API anahtarları, derleme katmanlarına yerleştirilmiş kimlik bilgileri ve imaj katmanlarına kopyalanıp unutulan yapılandırma dosyaları. \u003ccode\u003eleakwatch scan image\u003c/code\u003e, bir OCI veya Docker imajının her katmanını inceler ve bu sırları dağıtım öncesinde gün yüzüne çıkarır.\u003c/p\u003e\n\u003ch2 id=\"temel-kullanm\"\u003eTemel kullanım\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan image \u0026lt;image:tag\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eKomut tam olarak bir argüman alır: standart \u003ccode\u003ename:tag\u003c/code\u003e gösteriminde bir imaj referansı. Leakwatch imajları çekmek ve incelemek için \u003ca href=\"https://github.com/google/go-containerregistry\"\u003ego-containerregistry\u003c/a\u003e kullanır — herhangi bir Docker daemon \u003cstrong\u003egerekmez\u003c/strong\u003e.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Docker Hub imajını tara\nleakwatch scan image nginx:latest\n\n# Özel GitHub Container Registry imajını tara\nleakwatch scan image ghcr.io/org/myapp:v1.2.0\n\n# Amazon ECR imajını tara\nleakwatch scan image 123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"desteklenen-kayt-sunucular\"\u003eDesteklenen kayıt sunucuları\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eKayıt Sunucusu\u003c/th\u003e\n\u003cth\u003eÖrnek referans\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003eDocker Hub\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003enginx:latest\u003c/code\u003e, \u003ccode\u003emyorg/myapp:1.0.0\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eGitHub Container Registry (GHCR)\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eghcr.io/org/myapp:v1.2.0\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eAmazon ECR\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eGoogle Container Registry (GCR)\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003egcr.io/my-project/myapp:latest\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eOCI uyumlu herhangi bir kayıt sunucusu\u003c/td\u003e\n\u003ctd\u003eStandart \u003ccode\u003eregistry/name:tag\u003c/code\u003e biçimi\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"kimlik-dorulama\"\u003eKimlik doğrulama\u003c/h2\u003e\n\u003cp\u003eLeakwatch, Docker ve diğer OCI araçları tarafından kullanılan standart kimlik bilgisi anahtarlığını kullanır. \u003ccode\u003edocker login\u003c/code\u003e (veya \u003ccode\u003ecrane\u003c/code\u003e, \u003ccode\u003eskopeo\u003c/code\u003e, bulut sağlayıcısı kimlik bilgisi yardımcıları gibi eşdeğer araçlar) ile oturum açtıysanız, Leakwatch bu kimlik bilgilerini otomatik olarak kullanır.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Önce GHCR'a giriş yapın\ndocker login ghcr.io\n\n# Ardından tarayın — kimlik bilgileri otomatik olarak alınır\nleakwatch scan image ghcr.io/org/private-app:latest\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eAmazon ECR için, taramadan önce ECR kimlik bilgisi yardımcısını yapılandırın ya da \u003ccode\u003eAWS_ACCESS_KEY_ID\u003c/code\u003e ve ilgili ortam değişkenlerini ayarlayın.\u003c/p\u003e\n\u003ch2 id=\"tarama-nasl-alr\"\u003eTarama nasıl çalışır\u003c/h2\u003e\n\u003cp\u003eLeakwatch imaj manifestini çeker, her katmanı sırayla işler ve her katmandaki dosyaları çıkarır. Her dosyanın içeriği, dosya sistemi taramasıyla aynı tespit hattından geçirilir. \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e içindeki \u003ccode\u003efilter.exclude-paths\u003c/code\u003e yol dışlamaları (veya \u003ccode\u003e--exclude\u003c/code\u003e) burada da geçerlidir ve katmanlar içinde hangi dosya yollarının inceleneceğini sınırlar.\u003c/p\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNot\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eSıkıştırma bombası katmanlarına karşı bir savunma olarak Leakwatch, okuyacağı kümülatif sıkıştırması açılmış bayt miktarını sınırlar: katman başına 2 GiB ve tüm imaj genelinde 10 GiB. Bir katman veya imaj bu tavanı aşarsa, sıkıştırma açmanın sınırsız çalışmasına izin vermek yerine etkilenen katmanın taraması kısaltılır (bir uyarı günlüğe kaydedilir).\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"bayraklar\"\u003eBayraklar\u003c/h2\u003e\n\u003cp\u003eİmaja özgü bayrak yoktur. Tüm ortak tarama bayrakları geçerlidir:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eKısa\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-f\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ejson\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktı biçimi: \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, \u003ccode\u003egithub\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-o\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estdout\u003c/td\u003e\n\u003ctd\u003eSonuçları stdout yerine bu dosyaya yaz.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-c\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCPU sayısı\u003c/td\u003e\n\u003ctd\u003eEşzamanlı çalışan sayısı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--max-file-size\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e10485760\u003c/code\u003e (10 MB)\u003c/td\u003e\n\u003ctd\u003eBu boyutu aşan dosyaları atla (bayt).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--show-raw\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktıda ham sır değerini göster.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eDışlanacak yol kalıbı. Tekrarlanabilir; \u003ccode\u003efilter.exclude-paths\u003c/code\u003e ile birleştirilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-detectors\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eBu çalıştırma için hariç tutulacak dedektör kimlikleri. Tekrarlanabilir; \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e ile birleştirilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--no-verify\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSır doğrulamasını devre dışı bırak.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYalnızca doğrulama ile aktif olduğu onaylanan bulguları raporla.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRaporlanacak minimum önem: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e, \u003ccode\u003ecritical\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--remediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHer bulguya düzeltme rehberi ekle.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eYol tabanlı dışlamalar \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e dosyasında \u003ccode\u003efilter.exclude-paths\u003c/code\u003e altında ya da her çalıştırma için \u003ccode\u003e--exclude\u003c/code\u003e ile yapılandırılır. Ayrıntılar için \u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e sayfasına bakın.\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003e--config\u003c/code\u003e ve \u003ccode\u003e--log-level\u003c/code\u003e (varsayılan \u003ccode\u003ewarn\u003c/code\u003e) kök bayrakları da geçerlidir.\u003c/p\u003e\n\u003ch2 id=\"rnekler\"\u003eÖrnekler\u003c/h2\u003e\n\u003cp\u003eDocker Hub imajını tarayın ve sonuçları tablo olarak yazdırın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan image alpine:3.20 --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eÖzel kayıt sunucusu imajını tarayın ve SARIF çıktısı kaydedin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan image ghcr.io/org/myapp:v1.2.0 --format sarif -o results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eYalnızca doğrulanmış aktif sırları gösterin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan image myapp:latest --only-verified --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eJSON çıktısına düzeltme rehberi dahil edin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan image myapp:latest --remediation --format json -o image-findings.json\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"bulgu-meta-verisi\"\u003eBulgu meta verisi\u003c/h2\u003e\n\u003cp\u003eİmaj taramasından elde edilen her bulgu katman meta verisi içerir:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eAlan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eimage\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTaranan imaj referansı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003elayer\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBulgunun tespit edildiği katman özeti (imaj yapılandırmasındaki bir bulgu için \u003ccode\u003e\u0026quot;config\u0026quot;\u003c/code\u003e, katman değil).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003elayer_idx\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eİmaj manifestindeki katmanın sayısal indeksi (0 tabanlı); imaj yapılandırmasındaki bir bulgu için \u003ccode\u003e-1\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003efile_path\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eKatman içindeki dosyanın yolu.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eİpucu\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eGizli bilgilerin bir kayıt sunucusuna push edilmeden önce yakalanması için konteyner imaj taramasını CI/CD hattınızın derleme aşamasına entegre edin. Sonuçları doğrudan GitHub Code Scanning'e yüklemek için \u003ccode\u003e--format sarif\u003c/code\u003e kullanın.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"k-kodlar\"\u003eÇıkış kodları\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eKod\u003c/th\u003e\n\u003cth\u003eAnlam\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlandı, bulgu yok.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlandı, bulgular raporlandı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama başarısız oldu (imaj bulunamadı, kimlik doğrulama hatası, vb.).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlanmadan kesildi (\u003ccode\u003eCtrl+C\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e) ve hiçbir bulgu raporlanmamıştı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eHer çalıştırmanın ardından stderr'e bir tarama özeti yazdırılır. Taramalar SIGINT/SIGTERM sinyalinde düzgün biçimde iptal edilir.\u003c/p\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eHızlı Başlangıç\u003c/a\u003e — ilk taramanızı bir dakikadan kısa sürede çalıştırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/filesystem\"\u003eDosya Sistemi\u003c/a\u003e — yerel bir dizin ağacını tarayın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e — dışlamaları ve diğer varsayılanları yapılandırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/ignoring-findings\"\u003eBulguları Yoksayma\u003c/a\u003e — bilinen yanlış pozitifleri bastırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eDoğrulama Nasıl Çalışır\u003c/a\u003e — doğrulama durumlarını anlayın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Referansı\u003c/a\u003e — tüm komutlar için tam bayrak referansı.\u003c/li\u003e\n\u003c/ul\u003e\n"},"scanning/filesystem":{"title":"Dosya Sistemi","description":"leakwatch scan fs komutuyla yerel bir dizin ağacını sızan sırlara karşı tarayın.","html":"\u003ch1 id=\"dosya-sistemi\"\u003eDosya Sistemi\u003c/h1\u003e\n\u003cp\u003eSırlar çoğu zaman önce yerel kaynak kodda ortaya çıkar. \u003ccode\u003eleakwatch scan fs\u003c/code\u003e komutu, bir dizin ağacındaki tüm dosyaları dolaşır, her biri üzerinde tam tespit hattını çalıştırır ve bulguları raporlar — henüz commit edilmeden önce yakalamak ya da mevcut bir kod tabanını sonradan taramak için kullanabilirsiniz.\u003c/p\u003e\n\u003ch2 id=\"temel-kullanm\"\u003eTemel kullanım\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs [path...]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003epath\u003c/code\u003e, \u003cstrong\u003esıfır veya daha fazla\u003c/strong\u003e argüman kabul eder; atlandığında Leakwatch geçerli çalışma dizinini (\u003ccode\u003e.\u003c/code\u003e) tarar. Her yol bir dizin (özyinelemeli olarak dolaşılır) veya tek bir dosya olabilir — tek bir çağrıda dosyaları ve dizinleri karıştırıp birden fazlasını geçebilirsiniz.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Geçerli dizini tara\nleakwatch scan fs\n\n# Belirli bir proje klasörünü tara\nleakwatch scan fs ./my-project\n\n# Tek bir dosyayı tara\nleakwatch scan fs cmd/main.go\n\n# Birden fazla dosya ve dizini birlikte tara\nleakwatch scan fs cmd/ main.go internal/config\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"dosya-sistemi-kaynann-otomatik-olarak-atladklar\"\u003eDosya sistemi kaynağının otomatik olarak atladıkları\u003c/h2\u003e\n\u003cp\u003eTaramaları hızlı ve gürültüsüz tutmak için dosya sistemi kaynağı herhangi bir yapılandırma gerekmeksizin şunları atlar:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eİkili dosyalar\u003c/strong\u003e — dosyanın ilk 8 KB'ında null byte bulunmasıyla tespit edilir.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eBilinen ikili uzantılar\u003c/strong\u003e — yaygın derlenmiş, görsel, ses, video ve arşiv biçimleri.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eKilit dosyaları\u003c/strong\u003e — \u003ccode\u003epackage-lock.json\u003c/code\u003e, \u003ccode\u003eyarn.lock\u003c/code\u003e, \u003ccode\u003ePipfile.lock\u003c/code\u003e ve benzerleri.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003e.git\u003c/code\u003e dizinleri\u003c/strong\u003e — Git nesne/paket deposu asla dolaşılmaz veya düz dosya olarak okunmaz, çünkü commit geçmişini taramanın özel yolu \u003ccode\u003escan git\u003c/code\u003e'tir. Bu, dolaşım sırasında karşılaşılan herhangi bir \u003ccode\u003e.git\u003c/code\u003e dizini için geçerlidir; \u003ccode\u003e.git\u003c/code\u003e'i tarama yolu olarak açıkça verirseniz bu muafiyet uygulanmaz ve normal şekilde taranır. Ayrıca etkilenmeyen bir diğer nokta: döngüleri önlemek için sembolik bağlantılar her zaman atlanır.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"bayraklar\"\u003eBayraklar\u003c/h2\u003e\n\u003ch3 id=\"dosya-sistemine-zg\"\u003eDosya sistemine özgü\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eTür\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring (tekrarlanabilir)\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eDışlanacak yollar için glob desenleri. Birden fazla kez belirtilebilir veya virgülle ayrılabilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"ortak-tarama-bayraklar\"\u003eOrtak tarama bayrakları\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eKısa\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-f\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ejson\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktı biçimi: \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, \u003ccode\u003egithub\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-o\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estdout\u003c/td\u003e\n\u003ctd\u003eSonuçları stdout yerine bu dosyaya yaz.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-c\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCPU sayısı\u003c/td\u003e\n\u003ctd\u003eEşzamanlı çalışan sayısı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--max-file-size\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e10485760\u003c/code\u003e (10 MB)\u003c/td\u003e\n\u003ctd\u003eBu boyutu aşan dosyaları atla (bayt).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--show-raw\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktıda ham sır değerini göster.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-detectors\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eBu çalıştırma için hariç tutulacak dedektör kimlikleri. Tekrarlanabilir; \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e ile birleştirilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--no-verify\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSır doğrulamasını devre dışı bırak.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYalnızca doğrulama ile aktif olduğu onaylanan bulguları raporla.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRaporlanacak minimum önem: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e, \u003ccode\u003ecritical\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--remediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHer bulguya düzeltme rehberi ekle.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003ccode\u003e--config\u003c/code\u003e ve \u003ccode\u003e--log-level\u003c/code\u003e (varsayılan \u003ccode\u003ewarn\u003c/code\u003e) kök bayrakları da geçerlidir.\u003c/p\u003e\n\u003ch2 id=\"rnekler\"\u003eÖrnekler\u003c/h2\u003e\n\u003cp\u003eGeçerli dizini tarayın ve terminalde renklendirilmiş bir tablo yazdırın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eTest dosyalarını ve vendor dizinlerini dışlayıp GitHub Code Scanning için SARIF çıktısı kaydedin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . \\\n --exclude \u0026quot;**/*_test.go\u0026quot; \\\n --exclude \u0026quot;vendor/**\u0026quot; \\\n --format sarif \\\n --output results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBüyük bir monorepo için dosya boyutunu sınırlayın ve çalışan sayısını artırın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --max-file-size 5242880 --concurrency 8 --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eYalnızca yüksek önem dereceli bulguları gösterip rotasyon talimatlarını dahil edin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --min-severity high --remediation --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"yollar-dlama\"\u003eYolları dışlama\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003e--exclude\u003c/code\u003e bayrağı glob desenlerini kabul eder ve birden fazla kez belirtilebilir ya da virgülle ayrılmış liste olarak kullanılabilir:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# İki ayrı bayrak\nleakwatch scan fs . --exclude \u0026quot;**/*_test.go\u0026quot; --exclude \u0026quot;docs/**\u0026quot;\n\n# Virgülle ayrılmış\nleakwatch scan fs . --exclude \u0026quot;**/*_test.go,docs/**\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eTakımınızla paylaşılan kalıcı dışlama kuralları için \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e dosyasına \u003ccode\u003efilter.exclude-paths\u003c/code\u003e altında ekleyin. Bu kurallar yalnızca dosya sistemi taramalarına değil, tüm kaynaklara uygulanır. Proje kök dizininizde bir \u003ccode\u003e.leakwatchignore\u003c/code\u003e dosyası da oluşturabilirsiniz. Ayrıntılar için \u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e ve \u003ca href=\"#/configuration/ignoring-findings\"\u003eBulguları Yoksayma\u003c/a\u003e sayfalarına bakın.\u003c/p\u003e\n\u003ch2 id=\"k-kodlar\"\u003eÇıkış kodları\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eKod\u003c/th\u003e\n\u003cth\u003eAnlam\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlandı, bulgu yok.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlandı, bulgular raporlandı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama başarısız oldu (yapılandırma hatası, okunamayan yol, vb.).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlanmadan kesildi (\u003ccode\u003eCtrl+C\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e) ve hiçbir bulgu raporlanmamıştı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eHer çalıştırmanın ardından stderr'e bir tarama özeti (kaynak türü, hedef, dosya sayısı, süre ve bulgu sayısı) yazdırılır. Taramalar SIGINT/SIGTERM sinyalinde düzgün biçimde iptal edilir.\u003c/p\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eİpucu\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eGeliştirme sırasında \u003ccode\u003eleakwatch scan fs . --format table\u003c/code\u003e komutunu çalıştırarak hızlı bir görsel genel bakış elde edin. CI hatlarında GitHub Code Scanning ile entegrasyon için \u003ccode\u003e--format sarif\u003c/code\u003e seçeneğine geçin.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eHızlı Başlangıç\u003c/a\u003e — ilk taramanızı bir dakikadan kısa sürede çalıştırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e — varsayılan biçimi, dışlamaları ve daha fazlasını yapılandırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/ignoring-findings\"\u003eBulguları Yoksayma\u003c/a\u003e — \u003ccode\u003e.leakwatchignore\u003c/code\u003e ve satır içi baskılama.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eDoğrulama Nasıl Çalışır\u003c/a\u003e — doğrulama durumlarını anlayın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/git-history\"\u003eGit Geçmişi\u003c/a\u003e — çalışma ağacı yerine commit edilmiş geçmişi tarayın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Referansı\u003c/a\u003e — tüm komutlar için tam bayrak referansı.\u003c/li\u003e\n\u003c/ul\u003e\n"},"scanning/git-history":{"title":"Git Geçmişi","description":"Yerel veya uzak bir Git deposunun tüm commit geçmişini sızan sırlara karşı tarayın.","html":"\u003ch1 id=\"git-gemii\"\u003eGit Geçmişi\u003c/h1\u003e\n\u003cp\u003eCommit edilip sonradan silinen bir sır, önceki her commit'te hâlâ mevcuttur ve depoya erişimi olan herkes tarafından ulaşılabilir durumdadır. \u003ccode\u003eleakwatch scan git\u003c/code\u003e, bir deponun — yerel veya uzak — \u003cem\u003etüm\u003c/em\u003e commit geçmişini dolaşarak bu sırları, istismar edilmeden önce gün yüzüne çıkarır.\u003c/p\u003e\n\u003ch2 id=\"temel-kullanm\"\u003eTemel kullanım\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git \u0026lt;url_or_path\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eKomut tam olarak bir argüman alır: depoya giden \u003cstrong\u003eyerel dosya sistemi yolu\u003c/strong\u003e (geçerli dizin için \u003ccode\u003e.\u003c/code\u003e) ya da \u003cstrong\u003euzak HTTP/HTTPS veya SSH URL'si\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eLeakwatch tüm Git işlemleri için \u003ca href=\"https://github.com/go-git/go-git\"\u003ego-git\u003c/a\u003e kullanır; bu, sistem \u003ccode\u003egit\u003c/code\u003e ikili dosyasına bağımlılığı olmayan saf bir Go uygulamasıdır.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Geçerli dizindeki yerel depoyu tara\nleakwatch scan git .\n\n# HTTPS üzerinden uzak bir depoyu tara\nleakwatch scan git https://github.com/org/repo.git\n\n# SSH üzerinden tara\nleakwatch scan git git@github.com:org/repo.git\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"tarama-nasl-alr\"\u003eTarama nasıl çalışır\u003c/h2\u003e\n\u003cp\u003eLeakwatch geçmişteki her commit'i dolaşır ve her commit tarafından eklenen blob'ları inceler. \u003cstrong\u003eBlob-hash tekilleştirmesi\u003c/strong\u003e, aynı dosya içeriğinin kaç commit tarafından referans alındığından bağımsız olarak yalnızca bir kez taranmasını sağlar. Bu, tarama süresini ham commit sayısı yerine depodaki \u003cem\u003ebenzersiz içerik\u003c/em\u003e miktarıyla orantılı tutar.\u003c/p\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNot\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eLeakwatch commit-bazlı diff'leri incelediğinden, sonradan silinen — yani mevcut çalışma ağacında görünmeyen — sırları da bulur.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"bayraklar\"\u003eBayraklar\u003c/h2\u003e\n\u003ch3 id=\"gite-zg\"\u003eGit'e özgü\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eTür\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--since\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring (YYYY-MM-DD)\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eYalnızca bu tarihten sonraki commit'leri tara.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--since-commit\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eYalnızca bu commit hash'inden HEAD'e kadar olan değişiklikleri tara (diff tabanlı).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--branch\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eVarsayılan yerine belirli bir dalı hedef al.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--depth\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eint\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e (tam)\u003c/td\u003e\n\u003ctd\u003e\u003cstrong\u003eYalnızca uzak depolar\u003c/strong\u003e için klonlama derinliği. \u003ccode\u003e0\u003c/code\u003e tam geçmişi tarar.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"ortak-tarama-bayraklar\"\u003eOrtak tarama bayrakları\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eKısa\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-f\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ejson\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktı biçimi: \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, \u003ccode\u003egithub\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-o\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estdout\u003c/td\u003e\n\u003ctd\u003eSonuçları stdout yerine bu dosyaya yaz.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-c\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCPU sayısı\u003c/td\u003e\n\u003ctd\u003eEşzamanlı çalışan sayısı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--max-file-size\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e10485760\u003c/code\u003e (10 MB)\u003c/td\u003e\n\u003ctd\u003eBu boyutu aşan blob'ları atla (bayt).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--show-raw\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktıda ham sır değerini göster.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eDışlanacak yol kalıbı. Tekrarlanabilir; \u003ccode\u003efilter.exclude-paths\u003c/code\u003e ile birleştirilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-detectors\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eBu çalıştırma için hariç tutulacak dedektör kimlikleri. Tekrarlanabilir; \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e ile birleştirilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--no-verify\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSır doğrulamasını devre dışı bırak.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYalnızca doğrulama ile aktif olduğu onaylanan bulguları raporla.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRaporlanacak minimum önem: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e, \u003ccode\u003ecritical\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--remediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHer bulguya düzeltme rehberi ekle.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003ccode\u003e--config\u003c/code\u003e ve \u003ccode\u003e--log-level\u003c/code\u003e (varsayılan \u003ccode\u003ewarn\u003c/code\u003e) kök bayrakları da geçerlidir.\u003c/p\u003e\n\u003ch2 id=\"rnekler\"\u003eÖrnekler\u003c/h2\u003e\n\u003cp\u003eYerel deponun tam geçmişini tarayın ve tablo olarak yazdırın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git . --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003edevelop\u003c/code\u003e dalında belirli bir tarihten sonraki commit'leri tarayın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git . --since 2026-02-23 --branch develop\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBelirli bir commit'ten bu yana tanıtılan değişiklikleri tarayın (CI'da yeni commit'leri kontrol etmek için kullanışlıdır):\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git . --since-commit a1b2c3d\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBüyük bir uzak depoyu hızlandırmak için sığ klonlama yapın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git https://github.com/org/repo.git --depth 50\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eUzak depoyu tarayıp yalnızca doğrulanmış bulguları SARIF olarak kaydedin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git https://github.com/org/repo.git \\\n --only-verified \\\n --format sarif \\\n --output git-results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"bulgu-meta-verisi\"\u003eBulgu meta verisi\u003c/h2\u003e\n\u003cp\u003eGit taramasından elde edilen her bulgu commit meta verisi içerir:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eAlan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003erepository\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTaranan deponun URL'si veya yolu (kimlik bilgileri ayıklanmış).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecommit\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSırrın tanıtıldığı commit hash'i.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eauthor\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYalnızca commit yazarının adı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eemail\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCommit yazarının e-posta adresi, ayrı bir alan olarak.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edate\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCommit zaman damgası.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ebranch\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDal bağlamı (kullanılabilir olduğunda).\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eİpucu\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003ePull request CI işlerinde yalnızca PR tarafından eklenen commit'leri taramak için \u003ccode\u003e--since-commit\u003c/code\u003e kullanın. Son aktiviteyi kapsayan zamanlanmış gece taramaları için \u003ccode\u003e--since \u0026lt;tarih\u0026gt;\u003c/code\u003e tercih edin.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"kimlik-bilgisi-gvenlii\"\u003eKimlik bilgisi güvenliği\u003c/h2\u003e\n\u003cp\u003eDepo URL'leri gömülü kimlik bilgileri içeriyorsa (örn. \u003ccode\u003ehttps://user:TOKEN@host/repo.git\u003c/code\u003e), Leakwatch bu bilgileri günlüklere veya çıktıya yazmadan önce URL'den ayırır; bu sayede token tarama sonuçlarında veya CI izlerinde hiçbir zaman görünmez.\u003c/p\u003e\n\u003ch2 id=\"k-kodlar\"\u003eÇıkış kodları\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eKod\u003c/th\u003e\n\u003cth\u003eAnlam\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlandı, bulgu yok.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlandı, bulgular raporlandı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama başarısız oldu (geçersiz URL, kimlik doğrulama hatası, vb.).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlanmadan kesildi (\u003ccode\u003eCtrl+C\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e) ve hiçbir bulgu raporlanmamıştı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eHer çalıştırmanın ardından stderr'e bir tarama özeti yazdırılır. Taramalar SIGINT/SIGTERM sinyalinde düzgün biçimde iptal edilir.\u003c/p\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eHızlı Başlangıç\u003c/a\u003e — ilk taramanızı bir dakikadan kısa sürede çalıştırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/multiple-repos\"\u003eÇoklu Depo\u003c/a\u003e — tek komutla birden fazla depoyu tarayın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/filesystem\"\u003eDosya Sistemi\u003c/a\u003e — geçmiş yerine çalışma ağacını tarayın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eDoğrulama Nasıl Çalışır\u003c/a\u003e — doğrulama durumlarını anlayın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/ignoring-findings\"\u003eBulguları Yoksayma\u003c/a\u003e — bilinen yanlış pozitifleri bastırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Referansı\u003c/a\u003e — tüm komutlar için tam bayrak referansı.\u003c/li\u003e\n\u003c/ul\u003e\n"},"scanning/multiple-repos":{"title":"Çoklu Depo","description":"Birden fazla Git deposunu eşzamanlı olarak tarayın ve sonuçları tek bir raporda birleştirin.","html":"\u003ch1 id=\"oklu-depo\"\u003eÇoklu Depo\u003c/h1\u003e\n\u003cp\u003eBir kuruluş büyüdükçe sırlar düzinelerce hatta yüzlerce deponun herhangi birine yerleşebilir. Bunları tek tek kontrol etmek pratik değildir. \u003ccode\u003eleakwatch scan repos\u003c/code\u003e, birden fazla depo URL'sini alır, bunları eşzamanlı olarak tarar ve tüm bulguları tek bir çıktıda birleştirir — tek komut, tek rapor.\u003c/p\u003e\n\u003ch2 id=\"temel-kullanm\"\u003eTemel kullanım\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan repos \u0026lt;url1\u0026gt; \u0026lt;url2\u0026gt; [url...]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eKomut \u003cstrong\u003een az iki\u003c/strong\u003e depo URL'si gerektirir. Tüm depolar otomatik olarak klonlanır, taranır ve temizlenir. Sonunda birleşik bulgu sayısı ve tek bir tarama özeti raporlanır.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan repos \\\n https://github.com/org/api.git \\\n https://github.com/org/web.git\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"nasl-alr\"\u003eNasıl çalışır\u003c/h2\u003e\n\u003cp\u003eLeakwatch aynı anda en fazla \u003ccode\u003e--parallel\u003c/code\u003e sayıda depo taraması başlatır. Her depo:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eSağlanan URL'den klonlanır (güvenlik açısından kimlik bilgileri günlüklerden ve çıktıdan ayıklanır).\u003c/li\u003e\n\u003cli\u003eTam tespit hattıyla taranır; bu depo için \u003ccode\u003e--concurrency\u003c/code\u003e sayıda çalışan kullanılır.\u003c/li\u003e\n\u003cli\u003eTarama tamamlandığında temizlenir (geçici klon silinir).\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eTüm depolardan elde edilen bulgular toplanır ve tek bir kaynaktan yapılmış tarama gibi tek bir çıktı olarak yazılır. Görüntülenen hedef \u003ccode\u003e\u0026lt;N\u0026gt; repositories\u003c/code\u003e (N depo) şeklindedir.\u003c/p\u003e\n\u003ch2 id=\"bayraklar\"\u003eBayraklar\u003c/h2\u003e\n\u003ch3 id=\"oklu-depoya-zg\"\u003eÇoklu depoya özgü\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eTür\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--parallel\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eint\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eEşzamanlı olarak taranacak depo sayısı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"ortak-tarama-bayraklar\"\u003eOrtak tarama bayrakları\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eKısa\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-f\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ejson\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktı biçimi: \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, \u003ccode\u003egithub\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-o\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estdout\u003c/td\u003e\n\u003ctd\u003eSonuçları stdout yerine bu dosyaya yaz.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-c\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCPU sayısı\u003c/td\u003e\n\u003ctd\u003e\u003cstrong\u003eDepo başına\u003c/strong\u003e eşzamanlı çalışan sayısı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--max-file-size\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e10485760\u003c/code\u003e (10 MB)\u003c/td\u003e\n\u003ctd\u003eBu boyutu aşan blob'ları atla (bayt).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--show-raw\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktıda ham sır değerini göster.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eHer deponun taramasından dışlanacak yol kalıbı. Tekrarlanabilir; \u003ccode\u003efilter.exclude-paths\u003c/code\u003e ile birleştirilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-detectors\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eBu çalıştırma için hariç tutulacak dedektör kimlikleri (örn. \u003ccode\u003eaws-access-key-id\u003c/code\u003e). Tekrarlanabilir; \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e ile birleştirilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--no-verify\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSır doğrulamasını devre dışı bırak.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYalnızca doğrulama ile aktif olduğu onaylanan bulguları raporla.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRaporlanacak minimum önem: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e, \u003ccode\u003ecritical\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--remediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHer bulguya düzeltme rehberi ekle.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003ccode\u003e.leakwatch.yaml\u003c/code\u003e dosyasındaki \u003ccode\u003efilter.exclude-paths\u003c/code\u003e yol dışlamaları (veya \u003ccode\u003e--exclude\u003c/code\u003e) tüm depolara uygulanır. \u003ccode\u003e--config\u003c/code\u003e ve \u003ccode\u003e--log-level\u003c/code\u003e (varsayılan \u003ccode\u003ewarn\u003c/code\u003e) kök bayrakları da geçerlidir.\u003c/p\u003e\n\u003ch2 id=\"rnekler\"\u003eÖrnekler\u003c/h2\u003e\n\u003cp\u003eİki depoyu tarayın ve sonuçları tablo olarak görüntüleyin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan repos \\\n https://github.com/org/api.git \\\n https://github.com/org/web.git \\\n --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBeş depoyu daha yüksek paralellik ile tarayın ve birleşik sonuçları SARIF olarak kaydedin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan repos \\\n https://github.com/org/api.git \\\n https://github.com/org/web.git \\\n https://github.com/org/infra.git \\\n https://github.com/org/mobile.git \\\n https://github.com/org/docs.git \\\n --parallel 4 \\\n --format sarif \\\n --output all-repos.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eDepo başına daha fazla çalışan kullanarak yalnızca doğrulanmış bulguları gösterin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan repos \\\n https://github.com/org/backend.git \\\n https://github.com/org/frontend.git \\\n --concurrency 8 \\\n --only-verified \\\n --format json \\\n --output verified-findings.json\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"paralellii-ayarlama\"\u003eParalelliği ayarlama\u003c/h2\u003e\n\u003cp\u003eVerimi kontrol eden iki parametre vardır:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e--parallel\u003c/code\u003e, kaç depo klonlama ve taramasının aynı anda çalışacağını kontrol eder. Varsayılan \u003ccode\u003e3\u003c/code\u003e, çoğu iş yükü için uygundur. Ağ bant genişliği ve CPU kapasitesi izin verdiğinde artırın; kısıtlı makinelerde düşürün.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e--concurrency\u003c/code\u003e (\u003ccode\u003e-c\u003c/code\u003e), her bir depodaki dosya blob'larını işleyen çalışan goroutine sayısını kontrol eder. Bu, tüm tarama komutlarında bulunan aynı bayraktır.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eTepe noktasındaki toplam eşzamanlı işlem = \u003ccode\u003e--parallel\u003c/code\u003e × \u003ccode\u003e--concurrency\u003c/code\u003e.\u003c/p\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNot\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eBir veya daha fazla depo taraması başarısız olursa (örneğin ağ hatası veya kimlik doğrulama sorunu nedeniyle), Leakwatch hatayı günlüğe kaydeder ve kalan depoları taramaya devam eder. \u003cstrong\u003eBulgular önceliklidir\u003c/strong\u003e: filtrelemeden sonra tüm çalıştırma genelinde tek bir bulgu bile kalırsa, bazı depoların taraması başarısız olsa dahi çıkış kodu \u003ccode\u003e1\u003c/code\u003e olur. Çıkış kodu \u003ccode\u003e2\u003c/code\u003e (depo tarama hatası) yalnızca genel olarak sıfır bulgu raporlandığında döndürülür.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"kimlik-bilgisi-gvenlii\"\u003eKimlik bilgisi güvenliği\u003c/h2\u003e\n\u003cp\u003eDepo URL'lerindeki gömülü kimlik bilgileri (örn. \u003ccode\u003ehttps://user:TOKEN@host/repo.git\u003c/code\u003e), URL günlüklere, çıktıya veya tarama özetine yazılmadan önce ayıklanır.\u003c/p\u003e\n\u003ch2 id=\"k-kodlar\"\u003eÇıkış kodları\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eKod\u003c/th\u003e\n\u003cth\u003eAnlam\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTüm depo taramaları tamamlandı, bulgu yok.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFiltrelemeden sonra bir veya daha fazla bulgu kaldı — bazı depoların taraması başarısız olsa veya çalıştırma kesintiye uğrasa bile döndürülür; böylece \u0026quot;sır bulundu\u0026quot; sinyali ilgisiz bir hata tarafından hiçbir zaman gizlenmez.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSıfır bulgu raporlandı ve bir veya daha fazla depo taraması başarısız oldu, ya da bir yapılandırma hatası oluştu.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇalıştırma tamamlanmadan kesildi (\u003ccode\u003eCtrl+C\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e) ve sıfır bulgu raporlanmıştı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eHer çalıştırmanın ardından stderr'e bir tarama özeti yazdırılır. Taramalar SIGINT/SIGTERM sinyalinde düzgün biçimde iptal edilir.\u003c/p\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/git-history\"\u003eGit Geçmişi\u003c/a\u003e — tek bir depoyu derinlemesine tarayın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eHızlı Başlangıç\u003c/a\u003e — ilk taramanızı bir dakikadan kısa sürede çalıştırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e — tüm kaynaklar için paylaşılan varsayılanları yapılandırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/ignoring-findings\"\u003eBulguları Yoksayma\u003c/a\u003e — bilinen yanlış pozitifleri bastırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eDoğrulama Nasıl Çalışır\u003c/a\u003e — doğrulama durumlarını anlayın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Referansı\u003c/a\u003e — tüm komutlar için tam bayrak referansı.\u003c/li\u003e\n\u003c/ul\u003e\n"},"scanning/slack":{"title":"Slack Çalışma Alanı","description":"Slack kanal ve DM mesaj metinlerini sızan sırlara karşı tarayın.","html":"\u003ch1 id=\"slack-alma-alan\"\u003eSlack Çalışma Alanı\u003c/h1\u003e\n\u003cp\u003eGeliştiriciler çoğu zaman kimlik bilgilerini sohbet üzerinden paylaşır — hızlı bir test için bir kanala yapıştırılan token, DM ile gönderilen parola ya da bir olay başlığında söz edilen API anahtarı. \u003ccode\u003eleakwatch scan slack\u003c/code\u003e, Slack çalışma alanınızdaki mesaj metinlerini okur ve bulduğu sırları işaretler.\u003c/p\u003e\n\u003cdiv class=\"callout callout-warn\"\u003e\u003cdiv class=\"callout-label\"\u003eUyarı\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eLeakwatch yalnızca \u003cstrong\u003emesaj metnini\u003c/strong\u003e tarar. Yüklenen dosyaların (ekler, snippet'ler) içeriğini taramak uygulanmamıştır. Yalnızca mesajların metin gövdesi analiz edilir.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"temel-kullanm\"\u003eTemel kullanım\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan slack\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBu komut \u003cstrong\u003ekonumsal argüman almaz\u003c/strong\u003e. Tüm yapılandırma bayraklar veya ortam değişkenleri aracılığıyla sağlanır.\u003c/p\u003e\n\u003ch2 id=\"kimlik-dorulama\"\u003eKimlik doğrulama\u003c/h2\u003e\n\u003cp\u003eBir Slack Bot Token gereklidir. \u003ccode\u003e--token\u003c/code\u003e bayrağı veya \u003ccode\u003eLEAKWATCH_SLACK_TOKEN\u003c/code\u003e ortam değişkeni aracılığıyla sağlayın. Ortam değişkeni kullanmak önerilir; böylece token kabuk geçmişinde veya süreç listelerinde asla görünmez.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eexport LEAKWATCH_SLACK_TOKEN=xoxb-...\nleakwatch scan slack\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"gerekli-bot-token-kapsamlar\"\u003eGerekli bot token kapsamları\u003c/h3\u003e\n\u003cp\u003eBot token'ı, aşağıdaki OAuth kapsamlarına sahip bir Slack uygulamasıyla ilişkilendirilmiş olmalıdır:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eKapsam\u003c/th\u003e\n\u003cth\u003eAmaç\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003echannels:history\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBotun katıldığı genel kanallardaki mesajları oku.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egroups:history\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBotun katıldığı özel kanallardaki mesajları oku.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eim:history\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDoğrudan mesajları oku (yalnızca \u003ccode\u003e--include-dms\u003c/code\u003e ile gerekli).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003empim:history\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGrup doğrudan mesajlarını oku (yalnızca \u003ccode\u003e--include-dms\u003c/code\u003e ile gerekli).\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"bayraklar\"\u003eBayraklar\u003c/h2\u003e\n\u003ch3 id=\"slacke-zg\"\u003eSlack'e özgü\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eTür\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eSlack Bot Token. \u003ccode\u003eLEAKWATCH_SLACK_TOKEN\u003c/code\u003e ortam değişkeni tercih edilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--channels\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003etüm kanallar\u003c/td\u003e\n\u003ctd\u003eTaranacak kanal adlarının virgülle ayrılmış listesi.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-channels\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eAtlanacak kanal adlarının virgülle ayrılmış listesi.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--since\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring (YYYY-MM-DD)\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eBu tarihte veya sonrasında gönderilen mesajları tara.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--include-dms\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ebool\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDoğrudan mesajları ve grup DM'lerini de tara.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--rate-limit\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003efloat\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSaniye başına maksimum Slack API istek sayısı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"ortak-tarama-bayraklar\"\u003eOrtak tarama bayrakları\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eKısa\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-f\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ejson\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktı biçimi: \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, \u003ccode\u003egithub\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-o\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estdout\u003c/td\u003e\n\u003ctd\u003eSonuçları stdout yerine bu dosyaya yaz.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-c\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCPU sayısı\u003c/td\u003e\n\u003ctd\u003eEşzamanlı çalışan sayısı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--max-file-size\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e10485760\u003c/code\u003e (10 MB)\u003c/td\u003e\n\u003ctd\u003eDahili parça boyutu sınırı (bayt).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--show-raw\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktıda ham sır değerini göster.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-detectors\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eBu çalıştırma için hariç tutulacak dedektör kimlikleri. Tekrarlanabilir; \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e ile birleştirilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--no-verify\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSır doğrulamasını devre dışı bırak.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYalnızca doğrulama ile aktif olduğu onaylanan bulguları raporla.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRaporlanacak minimum önem: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e, \u003ccode\u003ecritical\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--remediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHer bulguya düzeltme rehberi ekle.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003ccode\u003e--config\u003c/code\u003e ve \u003ccode\u003e--log-level\u003c/code\u003e (varsayılan \u003ccode\u003ewarn\u003c/code\u003e) kök bayrakları da geçerlidir. Diğer her tarama alt komutunun aksine, \u003ccode\u003escan slack\u003c/code\u003e komutunda \u003ccode\u003e--exclude\u003c/code\u003e yol-kalıbı bayrağı yoktur — bunun yerine tüm kanalları atlamak için \u003ccode\u003e--exclude-channels\u003c/code\u003e kullanın.\u003c/p\u003e\n\u003ch2 id=\"rnekler\"\u003eÖrnekler\u003c/h2\u003e\n\u003cp\u003eToken için ortam değişkeni kullanarak botun erişebildiği tüm kanalları tarayın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eexport LEAKWATCH_SLACK_TOKEN=xoxb-...\nleakwatch scan slack\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBelirli kanalları tarayın ve yılın başından bu yana gönderilen mesajlarla sınırlayın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan slack \\\n --channels general,engineering,backend \\\n --since 2026-01-01\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eGürültülü kanalları dışlayın ve doğrudan mesajları dahil edin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan slack \\\n --exclude-channels random,social,giphy \\\n --include-dms\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBüyük çalışma alanlarında Slack hız sınırı hatalarını önlemek için API istek hızını düşürün:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan slack --rate-limit 10 --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eYalnızca doğrulanmış aktif bulguları bir JSON dosyasına kaydedin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan slack \\\n --only-verified \\\n --format json \\\n --output slack-findings.json\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"bulgu-meta-verisi\"\u003eBulgu meta verisi\u003c/h2\u003e\n\u003cp\u003eSlack taramasından elde edilen her bulgu mesaj ve kanal meta verisi içerir:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eAlan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003echannel\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBulgunun tespit edildiği Slack kanalının \u003cstrong\u003ekimliği\u003c/strong\u003e (örn. \u003ccode\u003eC0123456\u003c/code\u003e) — okunabilir ad değil.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003echannel_name\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003echannel\u003c/code\u003e alanından ayrı, okunabilir kanal adı (örn. \u003ccode\u003eengineering\u003c/code\u003e).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003emessage_user\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMesaj yazarının Slack kullanıcı kimliği. Slack bulguları için \u003ccode\u003eauthor\u003c/code\u003e alanı yoktur.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003emessage_ts\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSlack mesaj zaman damgası (benzersiz mesaj kimliği).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ethread_ts\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYalnızca bulgu bir ileti dizisi yanıtındaysa bulunan, üst mesajın zaman damgası.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"performans-deerlendirmeleri\"\u003ePerformans değerlendirmeleri\u003c/h2\u003e\n\u003cp\u003eSlack API istekleri, Slack tarafından uygulanan hız sınırlarına tabidir. \u003ccode\u003e--rate-limit\u003c/code\u003e bayrağı (varsayılan saniyede \u003ccode\u003e1\u003c/code\u003e istek), Leakwatch'ın istekleri ne kadar agresif yapacağını kontrol eder. Cömert hız sınırlarına sahip bir çalışma alanında daha hızlı bir tarama için dikkatli bir şekilde artırın, ya da \u003ccode\u003e429 Too Many Requests\u003c/code\u003e hatası görüyorsanız daha da düşürün.\u003c/p\u003e\n\u003cp\u003eSlack \u003ccode\u003e429 Too Many Requests\u003c/code\u003e ile yanıt verdiğinde, Leakwatch \u003ccode\u003eRetry-After\u003c/code\u003e başlığına otomatik olarak uyar ve taramayı tamamen başarısız kılmak yerine isteği yeniden dener.\u003c/p\u003e\n\u003cp\u003eHer çalıştırmada tüm çalışma alanını taramak yerine belirli kanalları hedeflemek için \u003ccode\u003e--channels\u003c/code\u003e kullanın. Mesajları artımlı biçimde taramak için \u003ccode\u003e--since\u003c/code\u003e ile birleştirin.\u003c/p\u003e\n\u003ch2 id=\"k-kodlar\"\u003eÇıkış kodları\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eKod\u003c/th\u003e\n\u003cth\u003eAnlam\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlandı, bulgu yok.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlandı, bulgular raporlandı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama başarısız oldu (eksik token, kimlik doğrulama hatası, vb.).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlanmadan kesildi (\u003ccode\u003eCtrl+C\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e) ve hiçbir bulgu raporlanmamıştı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eHer çalıştırmanın ardından stderr'e bir tarama özeti yazdırılır. Taramalar SIGINT/SIGTERM sinyalinde düzgün biçimde iptal edilir.\u003c/p\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eHızlı Başlangıç\u003c/a\u003e — ilk taramanızı bir dakikadan kısa sürede çalıştırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e — \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e ile varsayılanları yapılandırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/ignoring-findings\"\u003eBulguları Yoksayma\u003c/a\u003e — bilinen yanlış pozitifleri bastırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eDoğrulama Nasıl Çalışır\u003c/a\u003e — doğrulama durumlarını anlayın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/git-history\"\u003eGit Geçmişi\u003c/a\u003e — commit edilmiş geçmişi sırlara karşı tarayın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Referansı\u003c/a\u003e — tüm komutlar için tam bayrak referansı.\u003c/li\u003e\n\u003c/ul\u003e\n"},"verification/how-verification-works":{"title":"Doğrulama Nasıl Çalışır","description":"Leakwatch'ın tespit edilen bir sırrın hâlâ aktif olup olmadığını nasıl teyit ettiği, hangi doğrulama modlarını kullandığı ve doğrulamanın nasıl yapılandırılacağı veya devre dışı bırakılacağı.","html":"\u003ch1 id=\"dorulama-nasl-alr\"\u003eDoğrulama Nasıl Çalışır\u003c/h1\u003e\n\u003cp\u003eBir kod tabanında sır bulmak hikayenin yalnızca yarısıdır. Altı ay önce döndürülen bir anahtar gürültüdür; hâlâ canlı olan bir anahtar ise aktif bir olayı temsil eder. Doğrulama, bu çizgiyi çizen adımdır — tespit edilen her bulguyu alır ve mümkün olan durumlarda sırrın sağlayıcıda hâlâ geçerli olup olmadığını teyit eder.\u003c/p\u003e\n\u003ch2 id=\"tespiten-dorulamaya\"\u003eTespiten doğrulamaya\u003c/h2\u003e\n\u003cp\u003eTarama motoru bulguları topladıktan sonra doğrulayıcı havuzu onları işlemeye alır. Her bulgu bir \u003ccode\u003edetector_id\u003c/code\u003e taşır; Leakwatch bu ID için kayıtlı bir doğrulayıcı olup olmadığını arar:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eBir doğrulayıcı mevcutsa çalışır ve bir durum döndürür.\u003c/li\u003e\n\u003cli\u003eO dedektör türü için kayıtlı bir doğrulayıcı yoksa bulgu değiştirilmeden \u003ccode\u003eunverified\u003c/code\u003e durumuyla geçer.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"ki-dorulama-modu\"\u003eİki doğrulama modu\u003c/h2\u003e\n\u003cp\u003eTüm sırlar aynı şekilde doğrulanamaz. Leakwatch, her kimlik bilgisi türü için güvenli olan yaklaşıma göre iki farklı yöntem kullanır.\u003c/p\u003e\n\u003ch3 id=\"canl-api-dorulamas\"\u003eCanlı API doğrulaması\u003c/h3\u003e\n\u003cp\u003e48 dedektör türü için Leakwatch, sağlayıcıya \u003cstrong\u003ekontrollü, salt-okunur bir API çağrısı\u003c/strong\u003e yapar — örneğin AWS anahtarları için \u003ccode\u003ests:GetCallerIdentity\u003c/code\u003e, GitHub token'ları için \u003ccode\u003eGET /user\u003c/code\u003e. Çağrı yalnızca kimliği doğrulamak için gereken minimum uç noktayı kullanır; hiçbir zaman veri değiştirmez, kaynak oluşturmaz veya faturalandırma olayı tetiklemez.\u003c/p\u003e\n\u003cp\u003eSağlayıcı başarılı bir yanıt döndürürse bulgu \u003ccode\u003everified_active\u003c/code\u003e olarak işaretlenir. Sağlayıcı kimlik bilgisini reddederse (örneğin HTTP 401 veya kapsamlı-anahtar hatalarını \u0026quot;inaktif\u0026quot; içinde birleştiren bir sağlayıcı için HTTP 403 ile) bulgu \u003ccode\u003everified_inactive\u003c/code\u003e olarak işaretlenir. Birkaç doğrulayıcı (örneğin SendGrid), kapsamı daraltılmış ama geçerli bir anahtardan kaynaklanan bir 403'ü gerçek bir reddediliş yanıtından ayırt eder ve bu durumda yine \u003ccode\u003everified_active\u003c/code\u003e bildirir — sağlayıcıya özgü notlar için \u003ca href=\"#/verification/verification-coverage\"\u003eDoğrulama Kapsamı\u003c/a\u003e sayfasına bakın.\u003c/p\u003e\n\u003ch3 id=\"yalnzca-format-dorulamas\"\u003eYalnızca format doğrulaması\u003c/h3\u003e\n\u003cp\u003eAltı kimlik bilgisi türü için güvenli bir canlı kontrol mevcut değildir — sağlayıcının anonim bir kimlik uç noktası yoktur, gerçek bir çağrı yan etkiye yol açar ya da (\u003ccode\u003ecoinbase-api-key\u003c/code\u003e için) canlı API, anahtarla güvenilir biçimde ilişkilendirilemeyen eşleştirilmiş bir sırla HMAC istek imzalaması gerektirir. Bu durumlar için Leakwatch, herhangi bir ağ isteği yapmadan kimlik bilgisinin yapısını doğrular:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eDedektör ID\u003c/th\u003e\n\u003cth\u003eDoğrulanan özellik\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egcp-service-account\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eJSON yapısı — \u003ccode\u003etype\u003c/code\u003e, \u003ccode\u003eproject_id\u003c/code\u003e, \u003ccode\u003eprivate_key_id\u003c/code\u003e, \u003ccode\u003eclient_email\u003c/code\u003e alanlarının varlığı\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003erabbitmq-connection-string\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAMQP URL'nin başarıyla ayrıştırılması\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esnowflake-credentials\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYalnızca format kontrolü — geçerli bir format hiçbir şeyi kanıtlamaz, sonuç her zaman \u003ccode\u003eunverified\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eazure-storage-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFormat kontrolü\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eazure-entra-secret\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFormat kontrolü\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecoinbase-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eKarakter kümesi ve uzunluk kontrolü\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNot\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eFormat kontrolü geçse bile sonuç \u003ccode\u003eunverified\u003c/code\u003e olarak kalır. Yapısal olarak geçerli bir kimlik bilgisi süresi dolmuş veya iptal edilmiş olabilir. Bu bulgular her zaman manuel inceleme gerektirir.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"dorulama-durumlar\"\u003eDoğrulama durumları\u003c/h2\u003e\n\u003cp\u003eLeakwatch çıktısındaki her bulgu dört durumdan birini taşır:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eDurum\u003c/th\u003e\n\u003cth\u003eAnlam\u003c/th\u003e\n\u003cth\u003eÖnerilen eylem\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003everified_active\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSırrın sağlayıcı tarafından canlı olduğu teyit edildi.\u003c/td\u003e\n\u003ctd\u003eAktif bir olay olarak ele alın. Hemen döndürün.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003everified_inactive\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSağlayıcı kimlik bilgisini reddetti.\u003c/td\u003e\n\u003ctd\u003eMuhtemelen zaten döndürülmüş. Bağlamı gözden geçirin ve kapatın.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eunverified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBu tür için doğrulayıcı yok, format doğrulaması sonuç vermedi veya doğrulama devre dışı bırakıldı.\u003c/td\u003e\n\u003ctd\u003eManuel olarak inceleyin; risk bağlama göre belirlenir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003everify_error\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDoğrulayıcı çalıştı ancak ağ hatası, zaman aşımı veya beklenmedik yanıtla karşılaştı.\u003c/td\u003e\n\u003ctd\u003ePotansiyel olarak aktif kabul edin. Yeniden deneyin veya manuel olarak inceleyin.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"dorulama-motoru\"\u003eDoğrulama motoru\u003c/h2\u003e\n\u003cp\u003eDoğrulama, tarama çalışan havuzundan yalıtılmış ayrı bir eşzamanlı çalışan havuzunda çalışır. Sağlayıcı hız sınırlarını tetiklememek için varsayılanlar temkinlidir:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eAyar\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eYapılandırma anahtarı\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003eÇalışan sayısı\u003c/td\u003e\n\u003ctd\u003e4\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003everification.concurrency\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eGlobal hız sınırı\u003c/td\u003e\n\u003ctd\u003e10 istek/saniye\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003everification.rate-limit\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eİstek başına zaman aşımı\u003c/td\u003e\n\u003ctd\u003e10 sn\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003everification.timeout\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eHer üç değer de \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e içindeki \u003ccode\u003everification:\u003c/code\u003e bloğu altında ayarlanabilir:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003everification:\n enabled: true\n concurrency: 4\n rate-limit: 10.0 # global, saniye başına istek sayısı\n timeout: 10s\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eİpucu\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eYüzlerce bulgu tetikleyen bir depoyu tarıyorsanız \u003ccode\u003erate-limit\u003c/code\u003e değerini 5'e düşürmeyi veya \u003ccode\u003e--only-verified\u003c/code\u003e etkinleştirmeyi düşünün; bu, doğrulanmış-aktif kümesini küçük ve uygulanabilir tutar.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"komut-satrndan-dorulamay-kontrol-etme\"\u003eKomut satırından doğrulamayı kontrol etme\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003e--no-verify\u003c/code\u003e ile \u003cstrong\u003edoğrulamayı tamamen devre dışı bırakın\u003c/strong\u003e (ya da yapılandırmada \u003ccode\u003everification.enabled: false\u003c/code\u003e ayarlayın). Her bulgu \u003ccode\u003eunverified\u003c/code\u003e olarak geçer. Bunu çevrimdışı veya hava boşluklu ortamlar için ya da herhangi bir sağlayıcı API'sine dokunmadan mümkün olan en hızlı taramayı istediğinizde kullanın.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --no-verify\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003cstrong\u003eYalnızca canlı olduğu doğrulanan sırları görmek\u003c/strong\u003e için \u003ccode\u003e--only-verified\u003c/code\u003e kullanın. \u003ccode\u003everified_active\u003c/code\u003e olmayan her şey çıktıdan düşürülür. Bu, büyük bir sonuç kümesini önceliklendirmenin en hızlı yoludur — yalnızca hemen harekete geçmeniz gereken anahtarları görürsünüz.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git . --only-verified\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-warn\"\u003e\u003cdiv class=\"callout-label\"\u003eUyarı\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003e\u003ccode\u003e--only-verified\u003c/code\u003e, \u003ccode\u003eunverified\u003c/code\u003e ve \u003ccode\u003everify_error\u003c/code\u003e bulgularını sessizce düşürür. Bunu uyumluluk bağlamında tek filtreniz olarak kullanmayın — bazı kimlik bilgisi türleri (JWT'ler, genel API anahtarları, özel anahtarlar) hiçbir zaman doğrulanamaz ve her zaman dışarıda kalır.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"sr-gvenlii\"\u003eSır güvenliği\u003c/h2\u003e\n\u003cp\u003eDoğrulama, ham sır değerinin süreç sınırını güvensiz biçimde asla terk etmeyecek şekilde tasarlanmıştır:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eDoğrulayıcılar sırrı TLS üzerinden doğrudan sağlayıcının HTTP uç noktasına iletir — diske yazılmaz, bir loga gönderilmez ve çalıştırmalar arasında önbelleğe alınmaz.\u003c/li\u003e\n\u003cli\u003eBaşlatılamayan veya panikle karşılaşan bir doğrulayıcı motor tarafından yakalanır; motor, bulguyu \u003ccode\u003everify_error\u003c/code\u003e olarak işaretler ve taramayı çökertmeden devam eder.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/verification/verification-coverage\"\u003eDoğrulama Kapsamı\u003c/a\u003e — hangi dedektör türlerinin canlı doğrulandığı, format doğrulandığı veya hiç doğrulanamadığı.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eYapılandırma: Yapılandırma Dosyası\u003c/a\u003e — \u003ccode\u003everification:\u003c/code\u003e bloğunun tam referansı.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/output/output-formats\"\u003eÇıktı Formatları\u003c/a\u003e — doğrulama durumunun JSON, SARIF, CSV ve tablo çıktısında nasıl göründüğü.\u003c/li\u003e\n\u003c/ul\u003e\n"},"verification/verification-coverage":{"title":"Doğrulama Kapsamı","description":"64 yerleşik dedektörün hangilerinin canlı doğrulandığı, yalnızca format doğrulandığı veya doğrulanamaz olduğu ve bunun önceliklendirme açısından ne anlama geldiği.","html":"\u003ch1 id=\"dorulama-kapsam\"\u003eDoğrulama Kapsamı\u003c/h1\u003e\n\u003cp\u003eLeakwatch 64 yerleşik dedektör ve 54 doğrulayıcı ile gelir; bu, \u003cstrong\u003e%84,4\u003c/strong\u003e kapsama oranı sağlar (64 dedektör türünden 54'ünde bir tür doğrulama mevcuttur — canlı ya da yalnızca format). Bu sayfa, çıktınızda ne beklemeniz gerektiğini bilmeniz için her dedektörü doğrulama durumuna göre eşler.\u003c/p\u003e\n\u003ch2 id=\"canl-dorulanan-48-dedektr-tr\"\u003eCanlı doğrulanan (48 dedektör türü)\u003c/h2\u003e\n\u003cp\u003eBu türler için Leakwatch, sağlayıcıya kontrollü, salt-okunur bir API çağrısı yapar ve \u003ccode\u003everified_active\u003c/code\u003e ya da \u003ccode\u003everified_inactive\u003c/code\u003e döndürür. Hiçbir veri oluşturulmaz veya değiştirilmez; çağrı, kimliği doğrulamak için gereken minimum uç noktayı kullanır.\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eDedektör türü\u003c/th\u003e\n\u003cth\u003eSağlayıcı\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eaws-access-key-id\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAWS STS (\u003ccode\u003eGetCallerIdentity\u003c/code\u003e)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egithub-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGitHub REST API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egithub-oauth-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGitHub REST API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egitlab-pat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGitLab REST API (token ile birlikte yakalanan kendi barındırılan bir GitLab sunucusu varsa onu hedefler; yoksa gitlab.com'a geri döner)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eslack-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSlack Web API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eopenai-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOpenAI API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eanthropic-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAnthropic API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edeepseek-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDeepSeek API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ehuggingface-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHugging Face API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esendgrid-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSendGrid Web API (kapsamı daraltılmış/kısıtlı bir anahtardan gelen \u003ccode\u003e403\u003c/code\u003e yanıtı, anahtarın kendisi geçerli olduğundan inaktif değil \u003ccode\u003everified_active\u003c/code\u003e olarak değerlendirilir — yalnızca \u003ccode\u003e401\u003c/code\u003e \u003ccode\u003everified_inactive\u003c/code\u003e olarak eşlenir)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003emailgun-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMailgun API (doğru AB veya ABD bölgesel uç noktasını otomatik olarak tespit edip çağırır)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epostmark-server-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePostmark API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003estripe-api-key-live\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eStripe API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003estripe-api-key-test\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eStripe API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edigitalocean-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDigitalOcean API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecloudflare-api-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCloudflare API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eheroku-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHeroku Platform API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003evercel-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eVercel REST API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003enpm-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003enpm Registry API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epypi-api-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePyPI API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003erubygems-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRubyGems API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edockerhub-pat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDocker Hub API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecircleci-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCircleCI API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eterraform-cloud-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTerraform Cloud API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ediscord-bot-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDiscord API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003etelegram-bot-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTelegram Bot API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esentry-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSentry API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epagerduty-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePagerDuty API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003enewrelic-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNew Relic API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egrafana-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGrafana API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edatadog-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDatadog API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esnyk-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSnyk API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003etwilio-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTwilio API (API Key Secret'ıyla eşleştirilmiş API Key SID ile kimlik doğrular; eşleştirilmiş sır olmadan sonuç, asla yanlış bir \u003ccode\u003everified_inactive\u003c/code\u003e değil, \u003ccode\u003eunverified\u003c/code\u003e olur)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edoppler-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDoppler API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003elaunchdarkly-sdk-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eLaunchDarkly API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esonarcloud-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSonarCloud API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eshopify-access-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eShopify Admin API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003enotion-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNotion API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003elinear-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eLinear API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003efigma-pat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFigma REST API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eairtable-pat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAirtable API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eokta-api-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOkta API (token ile birlikte yakalanan organizasyon alan adını hedefler)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eauth0-management-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAuth0 Management API (token'ın kendi JWT \u003ccode\u003eiss\u003c/code\u003e iddiasından çözülen kiracıyı hedefler)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edatabricks-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDatabricks REST API (token ile birlikte yakalanan çalışma alanı ana bilgisayarını çağırır)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ebitbucket-app-password\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBitbucket REST API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esupabase-service-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSupabase API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003einfura-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInfura API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eteams-webhook\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMicrosoft Teams\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"yalnzca-format-dorulamas-6-dedektr-tr\"\u003eYalnızca format doğrulaması (6 dedektör türü)\u003c/h2\u003e\n\u003cp\u003eBu doğrulayıcılar tamamen çevrimdışı çalışır. Hiçbir ağ isteği yapılmaz. Geçerli bir format kimlik bilgisinin aktif olduğunu kanıtlamadığından, altısı da format kontrolünün geçip geçmediğinden bağımsız olarak her zaman \u003ccode\u003eunverified\u003c/code\u003e döndürür.\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eDedektör ID\u003c/th\u003e\n\u003cth\u003eDoğrulanan özellik\u003c/th\u003e\n\u003cth\u003eNeden canlı kontrol yok\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egcp-service-account\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eJSON yapısı (\u003ccode\u003etype\u003c/code\u003e, \u003ccode\u003eproject_id\u003c/code\u003e, \u003ccode\u003eprivate_key_id\u003c/code\u003e, \u003ccode\u003eclient_email\u003c/code\u003e)\u003c/td\u003e\n\u003ctd\u003eCanlı kontrol, yan etkileri olan GCP OAuth2 token değişimi gerektirir\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003erabbitmq-connection-string\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAMQP URL'nin başarıyla ayrıştırılması\u003c/td\u003e\n\u003ctd\u003eHerkese açık kimlik doğrulamasız sağlık uç noktası yok\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esnowflake-credentials\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eParola uzunluğu ve host alt dize kontrolü\u003c/td\u003e\n\u003ctd\u003eCanlı kontrol bir JDBC/ODBC veritabanı bağlantısı gerektirir\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eazure-storage-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFormat kontrolü\u003c/td\u003e\n\u003ctd\u003eHesap başına HMAC imzalama gerektirir; genel kimlik uç noktası yok\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eazure-entra-secret\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFormat kontrolü\u003c/td\u003e\n\u003ctd\u003eİstemci kimlik bilgisi akışı oturum oluşturur\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecoinbase-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eKarakter kümesi ve uzunluk kontrolü\u003c/td\u003e\n\u003ctd\u003eCoinbase'in eski API'si, dedektörün anahtarla güvenilir biçimde ilişkilendiremeyeceği eşleştirilmiş sırrı gerektiren HMAC-SHA256 istek imzalamayla kimlik doğrular; canlı doğrulama denenmez, böylece gerçek bir anahtar hiçbir zaman yanlışlıkla inaktif olarak raporlanmaz\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"dorulanamaz-10-dedektr-tr\"\u003eDoğrulanamaz (10 dedektör türü)\u003c/h2\u003e\n\u003cp\u003eBu dedektör türlerinin hiç doğrulayıcısı yoktur. Bunlardan gelen bulgular her zaman \u003ccode\u003eunverified\u003c/code\u003e olur. Bu durum önemsiz oldukları anlamına \u003cstrong\u003egelmez\u003c/strong\u003e — tam olarak tespit edilip raporlanırlar — ancak herkese açık bir doğrulama API'si bulunmamakta ya da herhangi bir doğrulama girişimi yan etkiye yol açmaktadır.\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eDedektör ID\u003c/th\u003e\n\u003cth\u003eNeden\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ejwt\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eJWT herhangi bir tarafça yayınlanabilir; evrensel bir doğrulama uç noktası yoktur\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eprivate-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇağrılacak sağlayıcı yok; aktif kullanım uzaktan tespit edilemez\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egeneric-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTanım gereği bilinmeyen sağlayıcı\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edatabase-connection-string\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBağlanmak hedef veritabanında oturum oluşturur\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eredis-connection-string\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBağlanmak Redis örneğinde canlı bağlantı açar\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eftp-credentials\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGüvenli, salt-okunur FTP yoklama yöntemi yok\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eldap-credentials\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eLDAP bind kimliği doğrulanmış bir oturum oluşturur\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eslack-webhook\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eWebhook'un aktif olduğunu doğrulamak mesaj göndermeyi gerektirir\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ehashicorp-vault-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eVault token doğrulaması, Vault uç noktasının bilinmesini gerektirir\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ediscord-webhook-url\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBir webhook'un aktif olduğunu doğrulamak, ona bir mesaj göndermeyi gerektirir\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNot\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003e\u0026quot;Doğrulanamaz\u0026quot; \u0026quot;bulunamaz\u0026quot; anlamına gelmez. Bu 10 türün tamamı yine de tespit edilir ve çıktınızda görünür. Kimlik bilgisinin canlı olup olmadığını ve döndürülmesi gerekip gerekmediğini belirlemek için manuel inceleme gerektirir.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"kapsam-zeti\"\u003eKapsam özeti\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eKategori\u003c/th\u003e\n\u003cth\u003eSayı\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003eCanlı doğrulanan\u003c/td\u003e\n\u003ctd\u003e48\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eYalnızca format doğrulaması\u003c/td\u003e\n\u003ctd\u003e6\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eDoğrulanamaz\u003c/td\u003e\n\u003ctd\u003e10\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eToplam dedektör\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003e\u003cstrong\u003e64\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eDoğrulayıcı (herhangi bir kapsam)\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003e\u003cstrong\u003e54 (%84,4)\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eDoğrulama Nasıl Çalışır\u003c/a\u003e — iki doğrulama modu, durumlar ve doğrulama motoru.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/detectors/detector-catalog\"\u003eDedektör Kataloğu\u003c/a\u003e — yerleşik dedektörlerin tam listesi ve önem dereceleri.\u003c/li\u003e\n\u003c/ul\u003e\n"}}; +window.LW_MANUAL["tr"] = {"ci-cd/docker-usage":{"title":"Docker Kullanımı","description":"Resmi Docker imajını kullanarak Leakwatch taramalarını bir konteyner içinde çalıştırın.","html":"\u003ch1 id=\"docker-kullanm\"\u003eDocker Kullanımı\u003c/h1\u003e\n\u003cp\u003eResmi Leakwatch konteyner imajı, ana makineye herhangi bir şey kurmadan tarama yapmanızı sağlar. İmaj \u003ccode\u003eCGO_ENABLED=0\u003c/code\u003e ile statik olarak derlenmiş ve root olmayan bir kullanıcı olarak çalışır; bu nedenle kilitli CI ortamlarında ve ana sistemi değiştirmek istemediğiniz paylaşımlı makinelerde güvenle kullanılabilir.\u003c/p\u003e\n\u003ch2 id=\"maj-referans\"\u003eİmaj referansı\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eghcr.io/hodetech/leakwatch\n\u003c/code\u003e\u003c/pre\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eEtiket\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e:latest\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eEn son sürüm\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e:v1.8.0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGüncel sürüme tam sabitleme\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e:v1.7\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGüncel küçük sürüme sabitleme (yama sürümlerini takip eder)\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eİmaj Alpine tabanlıdır, root olmayan \u003ccode\u003eleakwatch\u003c/code\u003e kullanıcısı olarak çalışır, çalışma dizini olarak \u003ccode\u003e/scan\u003c/code\u003e kullanır ve giriş noktası olarak \u003ccode\u003eleakwatch\u003c/code\u003e'ı ayarlar.\u003c/p\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNot\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eGiriş noktası \u003ccode\u003eleakwatch\u003c/code\u003e olduğundan alt komutu ve bayrakları doğrudan imaj adının ardına eklersiniz — örneğin \u003ccode\u003eghcr.io/hodetech/leakwatch:latest scan fs /scan\u003c/code\u003e. İkili dosya adını tekrar yazmanıza gerek yoktur.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"yerel-dizin-tarama\"\u003eYerel dizin tarama\u003c/h2\u003e\n\u003cp\u003eTaramak istediğiniz dizini konteyner içindeki \u003ccode\u003e/scan\u003c/code\u003e dizinine bağlayın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003edocker run --rm \\\n -v \u0026quot;$(pwd):/scan\u0026quot; \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan fs /scan\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eAna makinedeki bir dosyaya sonuç yazmak için çıktı dosyasını bağlı birime yazın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003edocker run --rm \\\n -v \u0026quot;$(pwd):/scan\u0026quot; \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan fs /scan --format sarif -o /scan/leakwatch.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003eleakwatch.sarif\u003c/code\u003e dosyası, konteyner çıktıktan sonra ana makinedeki geçerli dizinde görünür.\u003c/p\u003e\n\u003ch2 id=\"uzak-git-deposu-tarama\"\u003eUzak Git deposu tarama\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003edocker run --rm \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan git https://github.com/org/repo.git --format json\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eUzak Git depoları için birim bağlaması gerekli değildir — Leakwatch bunları konteyner içindeki geçici bir dizine klonlar.\u003c/p\u003e\n\u003ch2 id=\"konteyner-imaj-tarama\"\u003eKonteyner imajı tarama\u003c/h2\u003e\n\u003cp\u003eLeakwatch daemonsuz çalışır: imaj katmanlarını Docker daemon'ına ihtiyaç duymadan doğrudan kayıt defterinden çeker. Bu, Leakwatch konteynerinden, ana makine Docker soketini bağlamadan uzak bir imajı tarayabileceğiniz anlamına gelir:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003edocker run --rm \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan image registry.example.com/my-app:v2.3.0\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eÖzel kayıt defterleri için kimlik bilgilerini, kayıt defterinizin desteklediği standart ortam değişkenleri aracılığıyla geçirin (örneğin, bağlı bir kimlik bilgisi dosyasına işaret eden \u003ccode\u003eDOCKER_CONFIG\u003c/code\u003e).\u003c/p\u003e\n\u003ch2 id=\"yaplandrma-dosyas-geirme\"\u003eYapılandırma dosyası geçirme\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003e.leakwatch.yaml\u003c/code\u003e dosyasını \u003ccode\u003e/scan\u003c/code\u003e dizinine bağlayın; Leakwatch onu otomatik olarak bulur:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003edocker run --rm \\\n -v \u0026quot;$(pwd):/scan\u0026quot; \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan fs /scan\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003e.leakwatch.yaml\u003c/code\u003e bağlanan dizinde olduğu sürece Leakwatch onu bulur çünkü \u003ccode\u003e/scan\u003c/code\u003e hem çalışma dizini hem de taramaya geçirilen yoldur. Yapılandırma dosyanız başka bir yerdeyse onu ayrıca bağlayın ve \u003ccode\u003e--config\u003c/code\u003e kullanın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003edocker run --rm \\\n -v \u0026quot;$(pwd):/scan\u0026quot; \\\n -v \u0026quot;/path/to/custom-config.yaml:/config/leakwatch.yaml:ro\u0026quot; \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan fs /scan --config /config/leakwatch.yaml\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"ortam-deikenleri-geirme\"\u003eOrtam değişkenleri geçirme\u003c/h2\u003e\n\u003cp\u003eBulut taraması ve token tabanlı kimlik doğrulama için ortam değişkenleri \u003ccode\u003e-e\u003c/code\u003e ile enjekte edilebilir:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# AWS kimlik bilgileriyle S3 taraması\ndocker run --rm \\\n -e AWS_ACCESS_KEY_ID=AKIA••••••••••••EXAMPLE \\\n -e AWS_SECRET_ACCESS_KEY=••••••••••••••••••••••••••••••••••••••• \\\n -e AWS_REGION=us-east-1 \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan s3 my-bucket\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eCI ortamlarında, kimlik bilgilerini komut satırına gömmek yerine maskelenmiş CI değişkenleri olarak enjekte etmeyi tercih edin.\u003c/p\u003e\n\u003ch2 id=\"kt-dosyas-kalb\"\u003eÇıktı dosyası kalıbı\u003c/h2\u003e\n\u003cp\u003eCI'da yaygın bir Docker kalıbı, sonuçları bağlı birime yazmak ve ardından dosyayı bir pipeline artifact'i olarak yüklemek veya arşivlemektir:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003edocker run --rm \\\n -v \u0026quot;$(pwd):/scan\u0026quot; \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan fs /scan \\\n --format json \\\n --only-verified \\\n -o /scan/leakwatch-results.json\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/installation\"\u003eKurulum\u003c/a\u003e — Docker kullanmak yerine yerel ikili dosyayı kurma.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/filesystem\"\u003eDosya Sistemi Taraması\u003c/a\u003e — \u003ccode\u003escan fs\u003c/code\u003e bayrakları ve davranışı.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/container-images\"\u003eKonteyner İmajları\u003c/a\u003e — OCI/Docker imaj katmanlarını sır açısından tarama.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/other-ci\"\u003eDiğer CI Sistemleri\u003c/a\u003e — GitLab CI ve diğer pipeline'larda Docker imajını kullanma.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Referansı\u003c/a\u003e — tüm alt komutlar için tam bayrak referansı.\u003c/li\u003e\n\u003c/ul\u003e\n"},"ci-cd/github-action":{"title":"GitHub Action","description":"GitHub iş akışlarında sır taraması yapmak için resmi Leakwatch GitHub Action'ını kullanın.","html":"\u003ch1 id=\"github-action\"\u003eGitHub Action\u003c/h1\u003e\n\u003cp\u003eDeponuza yapılan her push, bir sırrın içeri sızması için bir fırsattır. GitHub Marketplace'te yayımlanan ve \u003ccode\u003eHodeTech/Leakwatch@v1\u003c/code\u003e olarak kullanılan resmi \u003cstrong\u003eLeakwatch GitHub Action\u003c/strong\u003e, Leakwatch'ı doğrudan GitHub iş akışınıza entegre eder. Runner için önceden derlenmiş Leakwatch ikilisini indirir (Go araç zinciri veya derleme adımı gerekmez), taramayı çalıştırır, çıkış kodlarını işler, bir iş özeti (job summary) yazar ve isteğe bağlı olarak SARIF sonuçlarını GitHub Code Scanning'e yükler — bunların hepsini harici bir servis bağımlılığı olmadan yapar.\u003c/p\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNot\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003e\u003cstrong\u003eDesteklenen runner'lar:\u003c/strong\u003e action, Linux (\u003ccode\u003eubuntu-*\u003c/code\u003e) ve macOS (\u003ccode\u003emacos-*\u003c/code\u003e) runner'larında çalışır. Windows runner'ları henüz desteklenmemektedir; taramayı bir Linux/macOS runner'ında çalıştırın veya \u003ccode\u003eghcr.io/hodetech/leakwatch\u003c/code\u003e konteyner imajını kullanın.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"hzl-balang\"\u003eHızlı başlangıç\u003c/h2\u003e\n\u003cp\u003eSır bulunduğunda iş akışını engelleyen minimal yapılandırma:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e# .github/workflows/leakwatch-minimal.yml\nname: Sır taraması (minimal)\n\non: [push, pull_request]\n\njobs:\n leakwatch:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: HodeTech/Leakwatch@v1\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eYalnızca varsayılan değerlerle action, dosya sistemi taraması yapar (\u003ccode\u003escan-type: fs\u003c/code\u003e), SARIF çıktısı üretir, canlı doğrulamayı atlar (\u003ccode\u003eno-verify: true\u003c/code\u003e) ve herhangi bir bulgu raporlandığında işi başarısız kılar.\u003c/p\u003e\n\u003ch2 id=\"sarif-ykleme-ile-tam-rnek\"\u003eSARIF yükleme ile tam örnek\u003c/h2\u003e\n\u003cp\u003eAşağıdaki iş akışı, GitHub Code Scanning'e SARIF yüklemeyi etkinleştirir ve bulguları depo içinde güvenlik uyarıları olarak gösterir:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e# .github/workflows/leakwatch.yml\nname: Sır taraması\n\non:\n push:\n branches: [\u0026quot;main\u0026quot;, \u0026quot;develop\u0026quot;]\n pull_request:\n\npermissions:\n contents: read\n security-events: write # SARIF yüklemesi için gerekli\n\njobs:\n leakwatch:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n\n - name: Sırları tara\n uses: HodeTech/Leakwatch@v1\n with:\n scan-type: fs\n path: .\n format: sarif\n no-verify: \u0026quot;true\u0026quot;\n min-severity: low\n sarif-upload: \u0026quot;true\u0026quot;\n fail-on-findings: \u0026quot;true\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNot\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eSARIF yüklemesi, işin \u003ccode\u003epermissions: security-events: write\u003c/code\u003e bildirmesini gerektirir. Bu olmadan yükleme adımı 403 hatasıyla başarısız olur. \u003ccode\u003eactions/checkout@v4\u003c/code\u003e için \u003ccode\u003econtents: read\u003c/code\u003e izni de gereklidir.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"girdiler\"\u003eGirdiler\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eGirdi\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003escan-type\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efs\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇalıştırılacak tarama türü: \u003ccode\u003efs\u003c/code\u003e, \u003ccode\u003egit\u003c/code\u003e veya \u003ccode\u003eimage\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epath\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e.\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTaranacak yol (\u003ccode\u003efs\u003c/code\u003e/\u003ccode\u003egit\u003c/code\u003e için) veya imaj referansı (\u003ccode\u003eimage\u003c/code\u003e için).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eformat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003esarif\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktı biçimi: \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e veya \u003ccode\u003egithub\u003c/code\u003e (satır içi pull-request ek açıklamaları).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eoutput\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e``\u003c/td\u003e\n\u003ctd\u003eBiçimlendirilmiş çıktıyı bu dosyaya yaz (\u003ccode\u003eworking-directory\u003c/code\u003e'ye göreli). \u003ccode\u003eformat: github\u003c/code\u003e için yok sayılır. Boş ve \u003ccode\u003eformat: sarif\u003c/code\u003e ise varsayılan \u003ccode\u003eresults.sarif\u003c/code\u003e'tir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eonly-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYalnızca canlı doğrulama ile etkin olduğu teyit edilen bulguları raporla.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eno-verify\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003etrue\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSır doğrulamasını devre dışı bırak (sağlayıcılara giden ağ çağrısı yapılmaz).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003emin-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRaporlanacak minimum önem derecesi: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e veya \u003ccode\u003ecritical\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eremediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktıya giderme (remediation) rehberi ekle.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003econfig\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e``\u003c/td\u003e\n\u003ctd\u003eBir \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e yapılandırma dosyasının yolu.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003escan-diff\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eauto\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003egit\u003c/code\u003e taramalarında yalnızca olaya yeni gelen commit'leri tara. \u003ccode\u003eauto\u003c/code\u003e, bunu \u003ccode\u003epull_request\u003c/code\u003e/\u003ccode\u003epush\u003c/code\u003e olaylarında etkinleştirir; \u003ccode\u003etrue\u003c/code\u003e zorlar; \u003ccode\u003efalse\u003c/code\u003e her zaman tüm geçmişi tarar. \u003ccode\u003eactions/checkout\u003c/code\u003e ile \u003ccode\u003efetch-depth: 0\u003c/code\u003e gerektirir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eextra-args\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e``\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eleakwatch scan\u003c/code\u003e komutuna eklenen ek ham argümanlar (boşlukla ayrılmış). Action'ın kendi yönettiği bayraklar (\u003ccode\u003e--format\u003c/code\u003e, \u003ccode\u003e--output\u003c/code\u003e, \u003ccode\u003e--config\u003c/code\u003e, \u003ccode\u003e--show-raw\u003c/code\u003e) reddedilir — bunun yerine ilgili input'ları kullanın.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eworking-directory\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e.\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTaramanın çalıştırılacağı dizin.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esarif-upload\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTaramadan sonra SARIF sonuçlarını GitHub Code Scanning'e yükle.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003efail-on-findings\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003etrue\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBulgular raporlandığında (çıkış kodu 1) iş akışı adımını başarısız kıl. \u003ccode\u003efalse\u003c/code\u003e olarak ayarlandığında adım başarısız olmak yerine \u003ccode\u003e::warning::\u003c/code\u003e ek açıklaması yayar. Ciddi hatalar (çıkış kodu ≥ 2) bu ayardan bağımsız olarak her zaman adımı başarısız kılar.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eversion\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elatest\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eKurulacak Leakwatch sürümü: \u003ccode\u003elatest\u003c/code\u003e veya belirli bir sürümü sabitlemek için \u003ccode\u003ev1.8.0\u003c/code\u003e gibi bir etiket.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003erelease-repo\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eHodeTech/Leakwatch\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSürüm ikilisinin indirileceği depo (\u003ccode\u003eowner/name\u003c/code\u003e). Yalnızca fork veya kendi sunucunuzdaki aynalar için değiştirin.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"ktlar\"\u003eÇıktılar\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eÇıktı\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003efindings-count\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBulgu raporlanmadıysa \u003ccode\u003e0\u003c/code\u003e; bulgu raporlandıysa \u003ccode\u003e1\u003c/code\u003e. Leakwatch çıkış kodunu yansıtır.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esarif-file\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRunner üzerindeki SARIF çıktı dosyasının yolu (\u003ccode\u003eformat: sarif\u003c/code\u003e olduğunda ayarlanır).\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"cida-dorulama\"\u003eCI'da doğrulama\u003c/h2\u003e\n\u003cp\u003eVarsayılan olarak \u003ccode\u003eno-verify\u003c/code\u003e değeri \u003ccode\u003etrue\u003c/code\u003e'dur — CI'da canlı doğrulama \u003cstrong\u003ekapalıdır\u003c/strong\u003e. Bu, taramayı hızlı tutar ve CI runner'larından sağlayıcı API'lerine giden ağ çağrılarını önler; runner'lar güvenlik duvarı arkasında olabilir veya hız sınırlı kimlik bilgilerine sahip olabilir.\u003c/p\u003e\n\u003cp\u003eCI'da doğrulamayı etkinleştirmek için \u003ccode\u003eno-verify: \u0026quot;false\u0026quot;\u003c/code\u003e olarak ayarlayın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e- uses: HodeTech/Leakwatch@v1\n with:\n no-verify: \u0026quot;false\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-warn\"\u003e\u003cdiv class=\"callout-label\"\u003eUyarı\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eCI'da doğrulamayı etkinleştirmek, Leakwatch'ın her aday bulgu için sağlayıcılara (AWS, GitHub, Stripe vb.) kimlik doğrulamalı API çağrıları yapmasına neden olur. Sağlayıcı hız limitlerinden haberdar olun ve runner'ın giden internet erişimine sahip olduğundan emin olun.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"sarif-yklemesi-nasl-alr\"\u003eSARIF yüklemesi nasıl çalışır\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003esarif-upload: \u0026quot;true\u0026quot;\u003c/code\u003e ve \u003ccode\u003eformat: sarif\u003c/code\u003e olduğunda action:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eLeakwatch'a çıktıyı \u003ccode\u003eresults.sarif\u003c/code\u003e dosyasına yazmasını söyler.\u003c/li\u003e\n\u003cli\u003eTaramanın ardından \u003ccode\u003ecategory: leakwatch\u003c/code\u003e ile \u003ccode\u003egithub/codeql-action/upload-sarif@v3\u003c/code\u003e'ü çağırır.\u003c/li\u003e\n\u003cli\u003eGitHub dosyayı işler ve bulguları deponun \u003cstrong\u003eSecurity\u003c/strong\u003e sekmesinde \u003cstrong\u003eCode Scanning uyarıları\u003c/strong\u003e olarak gösterir.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eYükleme adımı \u003ccode\u003eif: always()\u003c/code\u003e ile çalışır; dolayısıyla \u003ccode\u003efail-on-findings: \u0026quot;true\u0026quot;\u003c/code\u003e tarama adımını başarısız kılsa bile sonuçlar yüklenir.\u003c/p\u003e\n\u003ch2 id=\"action-ktlarn-kullanmak\"\u003eAction çıktılarını kullanmak\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e- name: Sırları tara\n id: scan\n uses: HodeTech/Leakwatch@v1\n with:\n fail-on-findings: \u0026quot;false\u0026quot; # iş akışının devam etmesine izin ver\n\n- name: Sonucu yazdır\n run: echo \u0026quot;Raporlanan bulgular: ${{ steps.scan.outputs.findings-count }}\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"belirli-bir-srm-sabitleme\"\u003eBelirli bir sürümü sabitleme\u003c/h2\u003e\n\u003cp\u003eYeniden üretilebilir derlemeler için \u003ccode\u003eversion\u003c/code\u003e değerini belirli bir etikete sabitleyin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e- uses: HodeTech/Leakwatch@v1\n with:\n version: \u0026quot;v1.8.0\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBu, önceden derlenmiş \u003ccode\u003ev1.8.0\u003c/code\u003e ikilisini \u003ca href=\"https://github.com/HodeTech/Leakwatch/releases\"\u003eLeakwatch sürümlerinden\u003c/a\u003e indirir ve çalıştırmadan önce SHA-256 sağlama toplamını doğrular. En yüksek tedarik zinciri güvenliği için action'ın kendisini de bir commit SHA'sına sabitleyebilirsiniz: \u003ccode\u003euses: HodeTech/Leakwatch@\u0026lt;sha\u0026gt;\u003c/code\u003e.\u003c/p\u003e\n\u003ch2 id=\"yalnzca-deien-kodu-tarama-pull-request-diff\"\u003eYalnızca değişen kodu tarama (pull-request diff)\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003egit\u003c/code\u003e taramalarında action, taramayı bir pull request veya push'un gerçekten getirdiği commit'lerle sınırlayabilir; bu daha hızlıdır ve yalnızca yeni eklenen sırları yüzeye çıkarır. \u003ccode\u003escan-diff\u003c/code\u003e (varsayılan \u003ccode\u003eauto\u003c/code\u003e) ile kontrol edilir ve base commit'in yerel olarak bulunması için tam bir checkout gerektirir:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003ejobs:\n leakwatch:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n with:\n fetch-depth: 0 # PR base commit'inin mevcut olması için gerekli\n - uses: HodeTech/Leakwatch@v1\n with:\n scan-type: git\n path: .\n # scan-diff: auto (varsayılan) — pull_request/push'ta yalnızca base..HEAD taranır\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003epull_request\u003c/code\u003e olayında action \u003ccode\u003egithub.event.pull_request.base.sha\u003c/code\u003e'dan; \u003ccode\u003epush\u003c/code\u003e olayında \u003ccode\u003egithub.event.before\u003c/code\u003e'dan itibaren tarar. Her zaman tüm geçmişi taramak için \u003ccode\u003escan-diff: \u0026quot;false\u0026quot;\u003c/code\u003e, diff modunu zorlamak için \u003ccode\u003escan-diff: \u0026quot;true\u0026quot;\u003c/code\u003e kullanın. \u003ccode\u003escan-diff\u003c/code\u003e'in \u003ccode\u003efs\u003c/code\u003e/\u003ccode\u003eimage\u003c/code\u003e taramalarında etkisi yoktur.\u003c/p\u003e\n\u003ch2 id=\"satr-ii-pull-request-ek-aklamalar\"\u003eSatır içi pull-request ek açıklamaları\u003c/h2\u003e\n\u003cp\u003eBulguları GitHub Actions iş akışı komutları olarak yaymak için \u003ccode\u003eformat: github\u003c/code\u003e ayarlayın; bunlar pull request'in \u003cstrong\u003eFiles changed\u003c/strong\u003e görünümünde ve çalışma günlüğünde satır içi ek açıklamalar olarak görünür:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e- uses: HodeTech/Leakwatch@v1\n with:\n format: github\n fail-on-findings: \u0026quot;false\u0026quot; # isterseniz engellemeden yalnızca ek açıklama yapın\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eEk açıklamalar her zaman yalnızca \u003cstrong\u003emaskelenmiş\u003c/strong\u003e değeri gösterir — ham sır, (çoğu zaman herkese açık olan) PR arayüzüne veya günlüklere asla yazılmaz. Hızlı ve görünür PR geri bildirimi için \u003ccode\u003eformat: github\u003c/code\u003e, bulguları \u003cstrong\u003eSecurity\u003c/strong\u003e sekmesinde Code Scanning uyarıları olarak kaydetmek için \u003ccode\u003esarif-upload: true\u003c/code\u003e ile \u003ccode\u003eformat: sarif\u003c/code\u003e kullanın.\u003c/p\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/output/output-formats\"\u003eÇıktı Biçimleri\u003c/a\u003e — JSON, SARIF, CSV ve tablo çıktısını anlama.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/exit-codes\"\u003eÇıkış Kodları\u003c/a\u003e — çıkış kodlarının tarama sonuçlarıyla nasıl eşleştiği.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eDoğrulama Nasıl Çalışır\u003c/a\u003e — Leakwatch'ın sağlayıcı API'lerini ne zaman ve nasıl çağırdığı.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/pre-commit\"\u003ePre-commit Kancası\u003c/a\u003e — commit edilmeden önce sırları yakalama.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/other-ci\"\u003eDiğer CI Sistemleri\u003c/a\u003e — GitLab CI, Jenkins ve genel kabuk entegrasyonu.\u003c/li\u003e\n\u003c/ul\u003e\n"},"ci-cd/other-ci":{"title":"Diğer CI Sistemleri","description":"Leakwatch'ı GitLab CI, Jenkins, Bitbucket Pipelines ve diğer CI sistemlerine entegre edin.","html":"\u003ch1 id=\"dier-ci-sistemleri\"\u003eDiğer CI Sistemleri\u003c/h1\u003e\n\u003cp\u003eLeakwatch, çalışma zamanı bağımlılığı olmayan tek bir statik ikili dosya olduğundan, kabuk komutu çalıştırabilen herhangi bir CI ortamında çalışır: GitLab CI, Jenkins, Bitbucket Pipelines, CircleCI, Azure DevOps ve diğerleri. Bu sayfada açıklananların ötesinde bu sistemler için yerleşik bir entegrasyon yoktur; kalıp her zaman aynıdır: ikili dosyayı kur, taramayı çalıştır, çıkış koduna göre hareket et.\u003c/p\u003e\n\u003ch2 id=\"cida-leakwatch-kurma\"\u003eCI'da Leakwatch kurma\u003c/h2\u003e\n\u003cp\u003eRunner ortamınıza en uygun yöntemi seçin:\u003c/p\u003e\n\u003ch3 id=\"go-install-araclyla-runnerda-go-gerektirir\"\u003e\u003ccode\u003ego install\u003c/code\u003e aracılığıyla (runner'da Go gerektirir)\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003ego install github.com/HodeTech/leakwatch@latest\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eYeniden üretilebilir derlemeler için belirli bir sürüme sabitleyin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003ego install github.com/HodeTech/leakwatch@v1.8.0\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"docker-imaj-araclyla-go-gerekmez\"\u003eDocker imajı aracılığıyla (Go gerekmez)\u003c/h3\u003e\n\u003cp\u003e\u003ccode\u003eghcr.io/hodetech/leakwatch:latest\u003c/code\u003e'i iş imajı olarak kullanın veya \u003ccode\u003edocker run\u003c/code\u003e ile çalıştırın. Tam kalıp için \u003ca href=\"#/ci-cd/docker-usage\"\u003eDocker Kullanımı\u003c/a\u003e sayfasına bakın.\u003c/p\u003e\n\u003ch3 id=\"hazr-bir-srm-ikili-dosyas-araclyla\"\u003eHazır bir sürüm ikili dosyası aracılığıyla\u003c/h3\u003e\n\u003cp\u003eUygun tar arşivini \u003ca href=\"https://github.com/HodeTech/Leakwatch/releases\"\u003eGitHub Releases\u003c/a\u003e sayfasından indirin, çıkarın ve \u003ccode\u003ePATH\u003c/code\u003e'e ekleyin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003ecurl -LO https://github.com/HodeTech/Leakwatch/releases/latest/download/leakwatch_Linux_amd64.tar.gz\ntar -xzf leakwatch_Linux_amd64.tar.gz\nsudo mv leakwatch /usr/local/bin/leakwatch\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"k-kodlar\"\u003eÇıkış kodları\u003c/h2\u003e\n\u003cp\u003eLeakwatch, bir CI derlemesini başarısız kılmanın birincil mekanizması olan dört koddan biriyle çıkar:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eKod\u003c/th\u003e\n\u003cth\u003eAnlam\u003c/th\u003e\n\u003cth\u003eÖnerilen CI eylemi\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBulgu yok\u003c/td\u003e\n\u003ctd\u003ePipeline aşamasını geç\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSırlar bulundu\u003c/td\u003e\n\u003ctd\u003ePipeline aşamasını başarısız kıl\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCiddi hata (hatalı yapılandırma, okunamaz yol vb.)\u003c/td\u003e\n\u003ctd\u003ePipeline aşamasını başarısız kıl\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTamamlanmadan (\u003ccode\u003eCtrl+C\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e) kesildi, hiçbir bulgu raporlanmadı\u003c/td\u003e\n\u003ctd\u003ePipeline aşamasını başarısız kıl — tarama tamamlanmadığından temiz bir sonuca güvenilemez\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eÇıkış koduna göre dallanma yapan genel bir kabuk parçacığı:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eset +e\nleakwatch scan fs . --format json -o leakwatch.json --no-verify\nEXIT_CODE=$?\nset -e\n\nif [ \u0026quot;$EXIT_CODE\u0026quot; -eq 0 ]; then\n echo \u0026quot;Sır bulunamadı.\u0026quot;\nelif [ \u0026quot;$EXIT_CODE\u0026quot; -eq 1 ]; then\n echo \u0026quot;Sırlar bulundu — derlemeyi başarısız kılıyorum.\u0026quot;\n exit 1\nelse\n echo \u0026quot;Tarama hatası veya kesintisi (çıkış $EXIT_CODE) — derlemeyi başarısız kılıyorum.\u0026quot;\n exit \u0026quot;$EXIT_CODE\u0026quot;\nfi\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"gitlab-ci-rnei\"\u003eGitLab CI örneği\u003c/h2\u003e\n\u003cp\u003eAşağıdaki \u003ccode\u003e.gitlab-ci.yml\u003c/code\u003e işi Leakwatch'ı kurar, dosya sistemi taraması çalıştırır ve JSON raporunu pipeline artifact'i olarak saklar:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003eleakwatch:\n stage: test\n image: golang:1.25-alpine\n script:\n - go install github.com/HodeTech/leakwatch@v1.8.0\n - leakwatch scan fs . --format json -o leakwatch.json --no-verify\n artifacts:\n when: always\n paths:\n - leakwatch.json\n expire_in: 7 days\n allow_failure: false\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003eallow_failure: false\u003c/code\u003e (varsayılan) değeri, çıkış kodu \u003ccode\u003e1\u003c/code\u003e'in pipeline aşamasını başarısız kılması anlamına gelir. Taramanın merge işlemini engellemeden raporlamasını istiyorsanız \u003ccode\u003eallow_failure: true\u003c/code\u003e olarak ayarlayın.\u003c/p\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eİpucu\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eGitLab, SAST raporu artifact'larını destekler. Leakwatch SARIF üretir (\u003ccode\u003e--format sarif\u003c/code\u003e) ancak GitLab'ın yerel SAST JSON şemasını değil; bu nedenle \u003ccode\u003ereports: sast:\u003c/code\u003e anahtarı yerine \u003ccode\u003epaths:\u003c/code\u003e artifact yaklaşımını kullanın.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"ci-runnerlar-iin-neriler\"\u003eCI runner'ları için öneriler\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eGiden internet erişimi olmayan runner'larda \u003ccode\u003e--no-verify\u003c/code\u003e kullanın.\u003c/strong\u003e Doğrulama, sağlayıcılara (AWS, GitHub, Stripe vb.) canlı API çağrıları yapar. Hava boşluklu veya güvenlik duvarıyla kısıtlanmış runner'larda bu çağrılar zaman aşımına uğrar ve taramayı yavaşlatır. Doğrulamayı tamamen atlamak için \u003ccode\u003e--no-verify\u003c/code\u003e geçirin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --no-verify --format sarif -o results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003cstrong\u003eÇıktıyı artifact olarak kaydedin.\u003c/strong\u003e İşi tamamlandıktan sonra saklanabilecek, bir güvenlik açığı yönetim platformuna yüklenebilecek veya incelenebilecek bir dosya yazmak için \u003ccode\u003e--format sarif\u003c/code\u003e ya da \u003ccode\u003e--format json\u003c/code\u003e ile birlikte \u003ccode\u003e--output\u003c/code\u003e kullanın.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/strong\u003e değerini en çok önem taşıyan sırlara odaklanmak için ayarlayın. Gürültülü bir kod tabanında \u003ccode\u003e--min-severity high\u003c/code\u003e ile başlayın ve birikmiş öğeleri temizledikten sonra eşiği düşürün.\u003c/p\u003e\n\u003ch2 id=\"azure-devops-rnei\"\u003eAzure DevOps örneği\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e- script: |\n go install github.com/HodeTech/leakwatch@v1.8.0\n leakwatch scan fs . --format sarif -o $(Build.ArtifactStagingDirectory)/leakwatch.sarif --no-verify\n displayName: \u0026quot;Leakwatch sır taraması\u0026quot;\n\n- task: PublishBuildArtifacts@1\n inputs:\n pathToPublish: \u0026quot;$(Build.ArtifactStagingDirectory)\u0026quot;\n artifactName: \u0026quot;leakwatch-results\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"jenkins-rnei\"\u003eJenkins örneği\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-groovy\"\u003estage('Sır taraması') {\n steps {\n sh '''\n go install github.com/HodeTech/leakwatch@v1.8.0\n leakwatch scan fs . --format json -o leakwatch.json --no-verify\n '''\n archiveArtifacts artifacts: 'leakwatch.json', allowEmptyArchive: true\n }\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/reference/exit-codes\"\u003eÇıkış Kodları\u003c/a\u003e — tüm çıkış kodlarının tam referansı.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/output/output-formats\"\u003eÇıktı Biçimleri\u003c/a\u003e — JSON, SARIF, CSV ve tablo çıktısı.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/docker-usage\"\u003eDocker Kullanımı\u003c/a\u003e — ikili dosyayı kurmak yerine konteyner imajını kullanma.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/github-action\"\u003eGitHub Action\u003c/a\u003e — GitHub iş akışları için resmi action.\u003c/li\u003e\n\u003c/ul\u003e\n"},"ci-cd/pre-commit":{"title":"Pre-commit Kancası","description":"Her commit'ten önce sır taraması yapmak için Leakwatch pre-commit kancasını kullanın.","html":"\u003ch1 id=\"pre-commit-kancas\"\u003ePre-commit Kancası\u003c/h1\u003e\n\u003cp\u003eBir sırrı yakalamak için en ucuz an, onu depoya girmeden önce durdurmaktır. Leakwatch, her \u003ccode\u003egit commit\u003c/code\u003e işleminde \u003ccode\u003eleakwatch scan fs\u003c/code\u003e komutunu otomatik olarak çalıştıran yerel bir \u003ca href=\"https://pre-commit.com\"\u003epre-commit\u003c/a\u003e kancası sunar; böylece sızan bir API anahtarı veya parola, geçmişte yer almak yerine commit işlemini başarısız kılar.\u003c/p\u003e\n\u003ch2 id=\"n-koullar\"\u003eÖn koşullar\u003c/h2\u003e\n\u003cp\u003eŞunlara ihtiyacınız var:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003ePython 3.8+ (pre-commit bir Python aracıdır).\u003c/li\u003e\n\u003cli\u003eGenel olarak kurulmuş \u003ca href=\"https://pre-commit.com/#install\"\u003epre-commit\u003c/a\u003e (\u003ccode\u003epip install pre-commit\u003c/code\u003e veya \u003ccode\u003ebrew install pre-commit\u003c/code\u003e).\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ePATH\u003c/code\u003e üzerinde Go 1.25+ — kanca dili \u003ccode\u003egolang\u003c/code\u003e olduğundan pre-commit, ilk çalıştırmada Leakwatch'ı kaynaktan derler.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"yaplandrma\"\u003eYapılandırma\u003c/h2\u003e\n\u003cp\u003eDeponuzun köküne bir \u003ccode\u003e.pre-commit-config.yaml\u003c/code\u003e dosyası ekleyin (veya mevcut olanı genişletin):\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003erepos:\n - repo: https://github.com/HodeTech/Leakwatch\n rev: v1.8.0\n hooks:\n - id: leakwatch\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eKancaları yerel Git deposuna kurun:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003epre-commit install\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eHepsi bu kadar. Bundan itibaren her \u003ccode\u003egit commit\u003c/code\u003e işlemi bir dosya sistemi taraması tetikler. Leakwatch herhangi bir sır bulursa commit engellenir ve bulgular terminale yazdırılır.\u003c/p\u003e\n\u003ch2 id=\"elle-altrma\"\u003eElle çalıştırma\u003c/h2\u003e\n\u003cp\u003eTüm depoyu (yalnızca staged dosyaları değil) istediğiniz zaman taramak için:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003epre-commit run --all-files\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eDiğerlerini tetiklemeden yalnızca Leakwatch kancasını çalıştırmak için:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003epre-commit run leakwatch --all-files\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"ek-argmanlar-geirme\"\u003eEk argümanlar geçirme\u003c/h2\u003e\n\u003cp\u003eKancanın varsayılan davranışı, ek bayrak olmadan \u003ccode\u003eleakwatch scan fs\u003c/code\u003e'e karşılık gelir. \u003ccode\u003eargs:\u003c/code\u003e anahtarı aracılığıyla ek argümanlar geçirebilirsiniz:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003erepos:\n - repo: https://github.com/HodeTech/Leakwatch\n rev: v1.8.0\n hooks:\n - id: leakwatch\n args:\n - --only-verified\n - --min-severity\n - high\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBu örnek, yalnızca Leakwatch'ın hâlâ etkin olduğunu doğruladığı yüksek önem dereceli sırları raporlar — yanlış pozitif gürültüsünden kaçınmak isteyen ancak kapsam kaybetmek istemeyen ekipler için uygun katı bir politika.\u003c/p\u003e\n\u003cp\u003eDiğer kullanışlı argümanlar:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003eargs:\n - --no-verify # daha hızlı commit'ler için canlı doğrulamayı atla\n - --min-severity\n - medium # düşük önem dereceli gürültüyü bastır\n - --format\n - table # terminalde insan tarafından okunabilir çıktı\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNot\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eKanca tanımında \u003ccode\u003epass_filenames: false\u003c/code\u003e ayarlandığından kanca, yalnızca mevcut commit için staged dosyaları değil her zaman tam çalışma ağacını tarar. Bu, staged olmayan dosyalarda halihazırda bulunan sırların da tespit edileceğini garanti eder.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"kancann-taradklar\"\u003eKancanın taradıkları\u003c/h2\u003e\n\u003cp\u003eKanca, depo çalışma dizinine karşı \u003ccode\u003eleakwatch scan fs\u003c/code\u003e çalıştırır. CLI ile aynı tespit hattını kullanır: Aho-Corasick ön filtreleme, regex doğrulama, entropi hesaplama ve (\u003ccode\u003e--no-verify\u003c/code\u003e ayarlanmadıkça) canlı doğrulama.\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003e.leakwatch.yaml\u003c/code\u003e'daki yapılandırma otomatik olarak uygulanır — dışlama kalıpları, entropi eşikleri ve doğrulama ayarları, herhangi bir ek kanca yapılandırması olmadan geçerli olur.\u003c/p\u003e\n\u003ch2 id=\"kancay-geici-olarak-atlama\"\u003eKancayı geçici olarak atlama\u003c/h2\u003e\n\u003cp\u003eKancayı çalıştırmadan commit yapmak için (örneğin, maskelenmiş sır içeren bir test sabiti commit edilirken):\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eSKIP=leakwatch git commit -m \u0026quot;chore: test sabiti ekle\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-warn\"\u003e\u003cdiv class=\"callout-label\"\u003eUyarı\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003e\u003ccode\u003eSKIP=leakwatch\u003c/code\u003e kullanmak, o commit için tüm sır taramasını devre dışı bırakır. Yalnızca içeriğin güvenli olduğunu teyit ettiğinizde kullanın; kalıcı bastırmalar için bunun yerine \u003ccode\u003e.leakwatchignore\u003c/code\u003e veya satır içi \u003ccode\u003eleakwatch:ignore\u003c/code\u003e yorumlarını tercih edin.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"kanca-srmn-sabitli-tutma\"\u003eKanca sürümünü sabitli tutma\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003erev:\u003c/code\u003e değerini dal adı yerine belirli bir etikete sabitleyin. Bu, ekipteki tüm geliştiricilerin aynı dedektör setini kullandığını ve kancanın sprint ortasında sessizce yükseltilmediğini garantiler:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003erev: v1.8.0 # sabitle; 'main' veya 'HEAD' kullanmayın\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eGüncellemek için:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003epre-commit autoupdate\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBu komut \u003ccode\u003erev\u003c/code\u003e değerini en son etikete yükseltir ve siz onu commit etmeden önce değişikliği inceleme fırsatı tanır.\u003c/p\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/filesystem\"\u003eDosya Sistemi Taraması\u003c/a\u003e — kancanın çalıştırdığı temel tarama komutu.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e — \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e'da dışlamaları, entropiyi ve doğrulamayı kontrol etme.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/github-action\"\u003eGitHub Action\u003c/a\u003e — GitHub CI'da her push ve pull request'te tarama.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/exit-codes\"\u003eÇıkış Kodları\u003c/a\u003e — çıkış kodlarının tarama sonuçlarıyla nasıl eşleştiği.\u003c/li\u003e\n\u003c/ul\u003e\n"},"configuration/config-file":{"title":"Yapılandırma Dosyası","description":"Leakwatch'ı .leakwatch.yaml ile yapılandırma — tam şema, varsayılanlar, doğrulama kuralları, ortam değişkeni geçersiz kılmaları ve leakwatch init komutu.","html":"\u003ch1 id=\"yaplandrma-dosyas\"\u003eYapılandırma Dosyası\u003c/h1\u003e\n\u003cp\u003eLeakwatch'ın her tarama komutundaki davranışı, \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e adlı tek bir YAML dosyasıyla yönetilir. Bu dosyayı anlamak; eşzamanlılık, doğrulama, çıktı biçimi ve yol filtrelemeyi bir kez ayarlamanızı ve her taramanın bu ayarları otomatik olarak almasını sağlar.\u003c/p\u003e\n\u003ch2 id=\"dosya-kefi\"\u003eDosya keşfi\u003c/h2\u003e\n\u003cp\u003eLeakwatch, yapılandırma dosyasını aşağıdaki sırayla çözer:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003e--config \u0026lt;path\u0026gt;\u003c/code\u003e bayrağı\u003c/strong\u003e — çalışma dizininden bağımsız olarak açık bir yol kullanır.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eGeçerli dizin\u003c/strong\u003e — komutun çalıştırıldığı dizindeki \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAna dizin\u003c/strong\u003e — yedek olarak \u003ccode\u003e~/.leakwatch.yaml\u003c/code\u003e.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eHiçbir dosya bulunamazsa, her ayar için yerleşik varsayılanlar kullanılır. Bir dosya bulunur\u003cstrong\u003esa\u003c/strong\u003e (veya \u003ccode\u003e--config\u003c/code\u003e bir dosyayı işaret ederse) ama geçerli bir YAML olarak ayrıştırılamazsa, bu ölümcül bir hatadır — tarama sessizce varsayılanlara geri dönmez, çünkü bunu yapmak operatörün yapılandırdığı sınırların ötesinde tespit kapsamını sessizce genişletebilir.\u003c/p\u003e\n\u003ch2 id=\"balang-dosyas-oluturma\"\u003eBaşlangıç dosyası oluşturma\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eleakwatch init\u003c/code\u003e komutu, önerilen varsayılanlarla düzenlemeye hazır bir dosya yazar:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch init\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eVarsayılan olarak dosya, geçerli dizindeki \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e konumuna yazılır. Farklı bir yol seçmek için \u003ccode\u003e--output\u003c/code\u003e kullanın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch init --output /etc/leakwatch/.leakwatch.yaml\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eHedef dosya zaten mevcutsa, \u003ccode\u003eleakwatch init\u003c/code\u003e üzerine yazmayı reddeder ve hata vererek çıkar. Üzerine yazmak için \u003ccode\u003e--force\u003c/code\u003e kullanın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch init --force\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"ortam-deikeni-geersiz-klmalar\"\u003eOrtam değişkeni geçersiz kılmaları\u003c/h2\u003e\n\u003cp\u003eHer yapılandırma anahtarı bir ortam değişkeniyle geçersiz kılınabilir. İsimlendirme kuralı şudur:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eÖnek: \u003ccode\u003eLEAKWATCH_\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e.\u003c/code\u003e ve \u003ccode\u003e-\u003c/code\u003e karakterlerini \u003ccode\u003e_\u003c/code\u003e ile değiştirin\u003c/li\u003e\n\u003cli\u003eBüyük harfe çevirin\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eÖrnekler:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eYapılandırma anahtarı\u003c/th\u003e\n\u003cth\u003eOrtam değişkeni\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003escan.concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_SCAN_CONCURRENCY\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003everification.rate-limit\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_VERIFICATION_RATE_LIMIT\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eoutput.format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_OUTPUT_FORMAT\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edetection.entropy.threshold\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_DETECTION_ENTROPY_THRESHOLD\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"ncelik-sras\"\u003eÖncelik sırası\u003c/h2\u003e\n\u003cp\u003eAynı ayar birden fazla yerde belirtildiğinde, en yüksek öncelikli kaynak kazanır:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eKomut satırı bayrağı (en yüksek)\u003c/li\u003e\n\u003cli\u003eOrtam değişkeni\u003c/li\u003e\n\u003cli\u003eYapılandırma dosyası değeri\u003c/li\u003e\n\u003cli\u003eYerleşik varsayılan (en düşük)\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"tam-ema\"\u003eTam şema\u003c/h2\u003e\n\u003cp\u003eAşağıdaki açıklamalı şema, desteklenen her anahtarı, varsayılan değerini ve geçerli aralığını göstermektedir.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e# ── Tarama motoru ─────────────────────────────────────────────────────────────\n\nscan:\n # Eşzamanlı dosya işleme worker sayısı.\n # Varsayılan olarak ana makinedeki mantıksal CPU çekirdeği sayısı kullanılır.\n # \u0026gt;= 1 olmalıdır.\n concurrency: 8\n\n # Taranacak maksimum dosya boyutu (bayt cinsinden). Bu sınırı aşan dosyalar\n # tamamen atlanır. Varsayılan: 10 MB (10485760). \u0026gt;= 1 olmalıdır.\n max-file-size: 10485760\n\n# ── Tespit ────────────────────────────────────────────────────────────────────\n\ndetection:\n entropy:\n # Her aday eşleşme için Shannon entropi hesaplamasını etkinleştirir.\n enabled: true\n\n # Gösterim için kullanılan ve yerleşik generic-api-key dedektörüyle kendi\n # entropy alanına sahip her özel kuralı kapılayan entropi eşiği.\n # Aralık: 0–8. Varsayılan: 4.0.\n # Yerleşik bulgular hakkındaki nota bakın.\n threshold: 4.0\n\n# ── Doğrulama ─────────────────────────────────────────────────────────────────\n\nverification:\n # Sağlayıcı API'lerine karşı canlı doğrulamayı etkinleştirir.\n enabled: true\n\n # Sınırlı fallback ve istek kabulü dahil bulgu başına doğrulama operasyonu\n # zaman aşımı. Doğrulama etkinleştirildiğinde \u0026gt;= 1ms olmalıdır.\n # Süre dizesi kullanın (örn. \u0026quot;10s\u0026quot;, \u0026quot;500ms\u0026quot;) — tam sayı nanosaniye olarak\n # yorumlanır ve doğrulama başarısız olur.\n timeout: 10s\n\n # Eşzamanlı doğrulama worker sayısı. \u0026gt;= 1 olmalıdır.\n concurrency: 4\n\n # Saniyedeki maksimum doğrulama isteği (token-bucket hız sınırlayıcı).\n # \u0026gt; 0 olmalıdır.\n rate-limit: 10.0\n\n# ── Filtreleme ────────────────────────────────────────────────────────────────\n\nfilter:\n # Taramadan hariç tutulacak yollar için glob desenleri.\n # Desteklenen glob stilleri: filepath.Match desenleri, sıfır veya daha fazla\n # yol segmentini kapsayan ** çift yıldız ve herhangi bir derinlikte adlandırılmış\n # dizini eşleştiren sondaki eğik çizgili dir/ desenleri. Her desen hem tam yol\n # hem de temel dosya adına karşı test edilir.\n # Tüm tarama kaynaklarına uygulanır. (`scan slack` hariç her tarama alt\n # komutunda bulunan --exclude bayrağı, bu listenin yerine geçmek yerine\n # çalışma zamanında ona ekleme yapar.)\n # Varsayılan: [] (yerleşik ikili/kilit dosya atlamalarının ötesinde hariç tutma yok).\n exclude-paths:\n - \u0026quot;vendor/**\u0026quot;\n - \u0026quot;node_modules/**\u0026quot;\n - \u0026quot;**/*.min.js\u0026quot;\n - \u0026quot;**/*.min.css\u0026quot;\n - \u0026quot;go.sum\u0026quot;\n - \u0026quot;package-lock.json\u0026quot;\n - \u0026quot;yarn.lock\u0026quot;\n\n # Tamamen devre dışı bırakılacak dedektör ID'leri. Listelenen dedektörlerden\n # gelen bulgular, diğer ayarlardan bağımsız olarak hiçbir zaman üretilmez.\n # Varsayılan: [].\n # Her tarama alt komutunda bulunan --exclude-detectors bayrağı, bu listenin\n # yerine geçmek yerine çalışma zamanında ona ekleme yapar.\n exclude-detectors: []\n\n# ── Çıktı ─────────────────────────────────────────────────────────────────────\n\noutput:\n # Çıktı biçimi. Şunlardan biri: json, sarif, csv, table, github. Varsayılan: json.\n # --format / -f bayrağı bunu çalışma zamanında geçersiz kılar.\n format: json\n\n # Çıktıyı stdout yerine bu dosya yoluna yaz. Varsayılan: \u0026quot;\u0026quot; (stdout).\n # --output / -o bayrağı bunu çalışma zamanında geçersiz kılar. Uzantısız düz\n # bir yol, biçimin kendi uzantısıyla otomatik olarak tamamlanır ve dosya\n # 0600 izinleriyle yazılır. Her zaman stdout'a yazan \u0026quot;github\u0026quot; biçiminde\n # yok sayılır.\n file: \u0026quot;\u0026quot;\n\n # Bu önem seviyesinin altındaki bulguları bırak.\n # Şunlardan biri: low, medium, high, critical. Varsayılan: \u0026quot;\u0026quot; (tümünü göster).\n # --min-severity bayrağı bunu çalışma zamanında geçersiz kılar.\n severity-threshold: \u0026quot;\u0026quot;\n\n # Çıktıda maskelenmemiş sır değerini dahil et.\n # Varsayılan: false. --show-raw bayrağı bunu çalışma zamanında geçersiz kılar.\n show-raw: false\n\n# ── Özel kurallar ─────────────────────────────────────────────────────────────\n\n# Kendi dedektörlerinizi YAML kuralları olarak tanımlayın. Tam kural şeması\n# için özel kurallar sayfasına bakın.\n# custom-rules:\n# - id: \u0026quot;my-internal-token\u0026quot;\n# description: \u0026quot;Internal Service Token\u0026quot;\n# regex: \u0026quot;mycompany_[a-zA-Z0-9]{32}\u0026quot;\n# keywords: [\u0026quot;mycompany_\u0026quot;]\n# severity: critical\ncustom-rules: []\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNot\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003e\u003ccode\u003edetection.entropy.enabled\u003c/code\u003e, hesaplanan entropi değerinin bulguların yanında bulunup bulunmadığını kontrol eder. \u003ccode\u003edetection.entropy.threshold\u003c/code\u003e gösterimi kontrol etmez; motor entropi sezgisini tercih eden dedektörleri kapılar — şu anda yalnızca yerleşik \u003ccode\u003egeneric-api-key\u003c/code\u003e dedektörü. Entropisi eşiğin altına düşen bir eşleşme olası bir yer tutucu olarak bastırılır. Kendi \u003ccode\u003eentropy\u003c/code\u003e alanını bildiren özel kurallar bağımsız kural-başı eşiklerini uygular. Diğer her yapısal/biçim-çapalı yerleşik dedektör — \u003ccode\u003eaws-access-key-id\u003c/code\u003e, \u003ccode\u003egithub-token\u003c/code\u003e ve diğerleri — entropiden bağımsız olarak motor eşiği tarafından \u003cstrong\u003ehiçbir zaman\u003c/strong\u003e bırakılmaz.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"dorulama\"\u003eDoğrulama\u003c/h2\u003e\n\u003cp\u003eLeakwatch, taramaya başlamadan önce yüklenen yapılandırmayı doğrular ve aşağıdaki durumların herhangi birinde hata vererek çıkar:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eKoşul\u003c/th\u003e\n\u003cth\u003eHata\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003escan.concurrency \u0026lt; 1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGeçersiz eşzamanlılık değeri\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003escan.max-file-size \u0026lt; 1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGeçersiz max-file-size değeri\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eoutput.format\u003c/code\u003e \u003ccode\u003ejson|sarif|csv|table|github\u003c/code\u003e içinde değil\u003c/td\u003e\n\u003ctd\u003eDesteklenmeyen çıktı biçimi\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edetection.entropy.threshold\u003c/code\u003e 0–8 dışında\u003c/td\u003e\n\u003ctd\u003eGeçersiz entropi eşiği\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eoutput.severity-threshold\u003c/code\u003e geçerli bir seviye değil (boş değilse)\u003c/td\u003e\n\u003ctd\u003eGeçersiz severity-threshold\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003everification.timeout \u0026lt; 1ms\u003c/code\u003e (doğrulama etkinleştirildiğinde)\u003c/td\u003e\n\u003ctd\u003eGeçersiz doğrulama zaman aşımı\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003everification.concurrency \u0026lt; 1\u003c/code\u003e (doğrulama etkinleştirildiğinde)\u003c/td\u003e\n\u003ctd\u003eGeçersiz doğrulama eşzamanlılığı\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003everification.rate-limit \u0026lt;= 0\u003c/code\u003e (doğrulama etkinleştirildiğinde)\u003c/td\u003e\n\u003ctd\u003eGeçersiz doğrulama rate-limit\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/ignoring-findings\"\u003eBulguları Yok Sayma\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/severity-and-filtering\"\u003eÖnem Derecesi \u0026amp; Filtreleme\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/detectors/custom-rules\"\u003eÖzel Kurallar\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/environment-variables\"\u003eOrtam Değişkenleri\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n"},"configuration/ignoring-findings":{"title":"Bulguları Yok Sayma","description":".leakwatchignore dosyaları, satır içi yok sayma işaretçileri ve yerleşik ikili dosya ve kilit dosyası atlamaları ile yanlış pozitifleri bastırın.","html":"\u003ch1 id=\"bulgular-yok-sayma\"\u003eBulguları Yok Sayma\u003c/h1\u003e\n\u003cp\u003eHiçbir tarayıcının yanlış pozitif oranı sıfır değildir. Leakwatch, gürültüyü bastırmak için size üç katmanlı mekanizma sunar: yol tabanlı dışlamalar için bir \u003ccode\u003e.leakwatchignore\u003c/code\u003e dosyası, satır düzeyinde bastırma için satır içi işaretçiler ve ikili dosyalar ile yaygın kilit dosyaları için her zaman etkin olan yerleşik atlamalar.\u003c/p\u003e\n\u003ch2 id=\"leakwatchignore-dosyas\"\u003e\u003ccode\u003e.leakwatchignore\u003c/code\u003e dosyası\u003c/h2\u003e\n\u003cp\u003eTarama sonuçlarından yolları hariç tutmak için depo kökünüze (veya geçerli dizine) bir \u003ccode\u003e.leakwatchignore\u003c/code\u003e dosyası oluşturun. Gitignore stilinde söz dizimi kullanır:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e#\u003c/code\u003e ile başlayan satırlar yorum satırlarıdır.\u003c/li\u003e\n\u003cli\u003eBoş satırlar atlanır.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e!\u003c/code\u003e öneki bir deseni \u003cstrong\u003egeçersiz kılar\u003c/strong\u003e; önceki bir desen tarafından dışlanmış olacak bir yolu yeniden dahil eder.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSon eşleşen desen kazanır\u003c/strong\u003e — sıra önemlidir.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"ykleme-sras\"\u003eYükleme sırası\u003c/h3\u003e\n\u003cp\u003eLeakwatch, bulduğu \u003cstrong\u003eilk\u003c/strong\u003e \u003ccode\u003e.leakwatchignore\u003c/code\u003e dosyasını yükler; önce tarama kökünü, ardından yedek olarak geçerli çalışma dizinini kontrol eder. Her tarama için yalnızca bir dosya kullanılır — tarama kökünde bir \u003ccode\u003e.leakwatchignore\u003c/code\u003e varsa, geçerli dizindeki dosya (varsa) hiçbir zaman açılmaz, birleştirilmesi bir yana. Dosyalar arası öncelik veya birleştirme yoktur: öngörülebilir bir davranış için kurallarınızı tarama kökündeki tek bir \u003ccode\u003e.leakwatchignore\u003c/code\u003e dosyasında toplayın.\u003c/p\u003e\n\u003ch3 id=\"glob-sz-dizimi\"\u003eGlob söz dizimi\u003c/h3\u003e\n\u003cp\u003eÜç desen stili desteklenir:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eStil\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003cth\u003eÖrnek\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003eStandart glob\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efilepath.Match\u003c/code\u003e stili, hem tam yola hem de temel dosya adına karşı eşleştirilen\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e*.pem\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eÇift yıldız \u003ccode\u003e**\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSıfır veya daha fazla yol segmentini kapsar\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003etest/fixtures/**\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eSondaki eğik çizgi \u003ccode\u003edir/\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAdlandırılmış dizinin herhangi bir derinliğindeki her dosyayla eşleşir\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003esnapshots/\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"leakwatchignore-rnei\"\u003e\u003ccode\u003e.leakwatchignore\u003c/code\u003e örneği\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003e# Tüm test fixture dosyalarını yok say\ntest/fixtures/**\n\n# Dokümantasyondaki bilinen yer tutucu anahtarları yok say\ndocs/examples/\n\n# Ağaçtaki herhangi bir yerdeki belirli uzantılı dosyaları yok say\n*.pem.example\n\n# Yukarıdaki kural tarafından dışlanan belirli bir dosyayı yeniden dahil et\n!docs/examples/real-config-sample.yaml\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNot\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003e\u003ccode\u003e.leakwatchignore\u003c/code\u003e filtrelemesi, her bulgunun dosya yoluna göre tarama tamamlandıktan \u003cstrong\u003esonra\u003c/strong\u003e uygulanır. Dosyaların okunmasını engellemez — ürettikleri bulguları bastırır. Dosyaları okunmadan önce atlamak için yapılandırma dosyasında \u003ccode\u003efilter.exclude-paths\u003c/code\u003e veya \u003ccode\u003escan fs\u003c/code\u003e komutunda \u003ccode\u003e--exclude\u003c/code\u003e kullanın.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"satr-ii-yok-sayma-iaretileri\"\u003eSatır içi yok sayma işaretçileri\u003c/h2\u003e\n\u003cp\u003eSöz konusu satırdaki dedektörleri bastırmak için herhangi bir kaynak satırına doğrudan bir işaretçi koyun. İşaretçi satırın herhangi bir yerine yerleştirilebilir — genellikle bir yorum içinde — ve motor tarafından doğrulamadan \u003cstrong\u003eönce\u003c/strong\u003e uygulanır; böylece yok sayılan bir satır hiçbir zaman ağ çağrısını tetiklemez.\u003c/p\u003e\n\u003ch3 id=\"bir-satrdaki-tm-dedektrleri-bastr\"\u003eBir satırdaki tüm dedektörleri bastır\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-python\"\u003e# Ödeme işleme yapılandırması\nSTRIPE_KEY = \u0026quot;sk_test_XXXXXXXXXXXXXXXXXXXX\u0026quot; # leakwatch:ignore\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"bir-satrdaki-belirli-bir-dedektr-bastr\"\u003eBir satırdaki belirli bir dedektörü bastır\u003c/h3\u003e\n\u003cp\u003eYalnızca bir dedektörü bastırırken diğerlerini etkin bırakmak için \u003ccode\u003eleakwatch:ignore:\u0026lt;detector-id\u0026gt;\u003c/code\u003e kullanın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-go\"\u003e// Bu token dokümantasyon için kasıtlı olarak bir yer tutucudur\nexampleToken := \u0026quot;ghp_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\u0026quot; // leakwatch:ignore:github-token\n\u003c/code\u003e\u003c/pre\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e# Platform tarafından ayarlanan CI ortam değişkeni — gerçek bir sır değil\napi_key: \u0026quot;${CI_API_KEY_PLACEHOLDER}\u0026quot; # leakwatch:ignore:generic-api-key\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eİpucu\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eMümkün olduğunda genel form yerine dedektöre özgü formu (\u003ccode\u003eleakwatch:ignore:\u0026lt;detector-id\u0026gt;\u003c/code\u003e) tercih edin. Hangi dedektörü bastırdığınızı belgeler ve diğer tüm dedektörleri o satırda etkin bırakır.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"yerleik-atlamalar-her-zaman-uygulanr\"\u003eYerleşik atlamalar (her zaman uygulanır)\u003c/h2\u003e\n\u003cp\u003eLeakwatch, herhangi bir dedektörü çalıştırmadan önce aşağıdakileri koşulsuz olarak atlar:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eİkili dosya uzantıları\u003c/strong\u003e — \u003ccode\u003e.exe\u003c/code\u003e, \u003ccode\u003e.dll\u003c/code\u003e, \u003ccode\u003e.so\u003c/code\u003e, \u003ccode\u003e.dylib\u003c/code\u003e, \u003ccode\u003e.bin\u003c/code\u003e, \u003ccode\u003e.png\u003c/code\u003e, \u003ccode\u003e.jpg\u003c/code\u003e, \u003ccode\u003e.gif\u003c/code\u003e, \u003ccode\u003e.mp4\u003c/code\u003e, \u003ccode\u003e.zip\u003c/code\u003e, \u003ccode\u003e.tar\u003c/code\u003e, \u003ccode\u003e.gz\u003c/code\u003e, \u003ccode\u003e.pdf\u003c/code\u003e, \u003ccode\u003e.woff\u003c/code\u003e, \u003ccode\u003e.ttf\u003c/code\u003e ve diğerleri gibi uzantılara sahip dosyalar hiçbir zaman taranmaz.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eİkili içerik tespiti\u003c/strong\u003e — ilk 8 KB'ı null bayt içeren herhangi bir dosya, uzantısından bağımsız olarak ikili olarak kabul edilir ve atlanır.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eYaygın kilit dosyaları\u003c/strong\u003e — aşağıdaki dosya adları, yüksek oranda yanlış pozitif üreten hash ve sağlama toplamları içerdikleri için her zaman atlanır:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eDosya\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epackage-lock.json\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eyarn.lock\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epnpm-lock.yaml\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecomposer.lock\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eGemfile.lock\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eCargo.lock\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epoetry.lock\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ego.sum\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ePipfile.lock\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eBu yerleşik atlamalar devre dışı bırakılamaz. \u003ccode\u003efilter.exclude-paths\u003c/code\u003e ayarından ayrıdır ve yapılandırma tabanlı filtrelemeden önce çalışır.\u003c/p\u003e\n\u003ch2 id=\"tarama-ncesi-yol-tabanl-dlama\"\u003eTarama öncesi yol tabanlı dışlama\u003c/h2\u003e\n\u003cp\u003eYolları tarama motoru tarafından okunmadan önce dışlamak için yapılandırma dosyanızda \u003ccode\u003efilter.exclude-paths\u003c/code\u003e kullanın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003efilter:\n exclude-paths:\n - \u0026quot;vendor/**\u0026quot;\n - \u0026quot;node_modules/**\u0026quot;\n - \u0026quot;**/*.min.js\u0026quot;\n - \u0026quot;third-party/\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBu ayar \u003cstrong\u003etüm tarama kaynaklarına\u003c/strong\u003e uygulanır (dosya sistemi, Git geçmişi, konteyner imajları, bulut depolama, Slack). Yolları tarayan her \u003ccode\u003escan\u003c/code\u003e alt komutunda (\u003ccode\u003efs\u003c/code\u003e, \u003ccode\u003egit\u003c/code\u003e, \u003ccode\u003eimage\u003c/code\u003e, \u003ccode\u003es3\u003c/code\u003e, \u003ccode\u003egcs\u003c/code\u003e, \u003ccode\u003erepos\u003c/code\u003e — bunun yerine \u003ccode\u003e--exclude-channels\u003c/code\u003e ile dışlama yapan \u003ccode\u003eslack\u003c/code\u003e hariç) komut satırında da \u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e geçirebilirsiniz; tekrarlanabilir olan bu bayrak, \u003ccode\u003efilter.exclude-paths\u003c/code\u003e'in yerine geçmek yerine onunla birleştirilir.\u003c/p\u003e\n\u003cp\u003eYapılandırma dosyasını düzenlemeden gürültülü bir dedektörü tek bir çalıştırma için susturmak üzere, herhangi bir tarama alt komutunda \u003ccode\u003e--exclude-detectors \u0026lt;id\u0026gt;[,\u0026lt;id\u0026gt;...]\u003c/code\u003e (tekrarlanabilir) geçirin — bkz. \u003ca href=\"#/detectors/detector-catalog\"\u003eDedektör Kataloğu\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eTam yapılandırma şeması için \u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e, dedektör düzeyinde ve önem derecesi düzeyinde filtreleme için \u003ca href=\"#/configuration/severity-and-filtering\"\u003eÖnem Derecesi \u0026amp; Filtreleme\u003c/a\u003e bölümlerine bakın.\u003c/p\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/severity-and-filtering\"\u003eÖnem Derecesi \u0026amp; Filtreleme\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n"},"configuration/severity-and-filtering":{"title":"Önem Derecesi \u0026 Filtreleme","description":"Önem eşikleri, yalnızca doğrulanmış mod, dedektör dışlamaları ve yol dışlamaları kullanarak hangi bulguların çıktınıza ulaşacağını kontrol edin.","html":"\u003ch1 id=\"nem-derecesi--filtreleme\"\u003eÖnem Derecesi \u0026amp; Filtreleme\u003c/h1\u003e\n\u003cp\u003eYoğun bir kod tabanı çok sayıda bulgu üretebilir. Leakwatch, en önemli sinyallere odaklanmak için birleştirebileceğiniz birkaç bağımsız filtre sunar: önem eşikleri düşük öncelikli gürültüyü eler, yalnızca doğrulanmış mod yalnızca onaylanmış canlı sırları ortaya çıkarır, dedektör dışlamaları bilinen yanlış pozitif kaynakları susturur ve yol dışlamaları tüm dizin ağaçlarını kapsamın dışında bırakır.\u003c/p\u003e\n\u003ch2 id=\"nem-seviyeleri\"\u003eÖnem seviyeleri\u003c/h2\u003e\n\u003cp\u003eHer yerleşik dedektör, varsayılan bir önem derecesiyle birlikte gelir. En düşükten en yüksek önceliğe doğru dört seviye şunlardır:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eSeviye\u003c/th\u003e\n\u003cth\u003eTipik kullanım\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDaha yüksek yanlış pozitif oranına sahip genel desenler\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003emedium\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTanınabilir kimlik bilgisi biçimleri, doğrulanmamış\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ehigh\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMaruziyetin büyük olasılıkla önemli olduğu iyi yapılandırılmış sırlar\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecritical\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOnaylanmış canlı sırlar veya neredeyse sıfır yanlış pozitif oranlı biçimler\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eHer dedektöre atanan önem derecesi \u003ca href=\"#/detectors/detector-catalog\"\u003eDedektör Kataloğu\u003c/a\u003e'nda listelenmiştir.\u003c/p\u003e\n\u003ch2 id=\"--min-severity-eiin-altndaki-bulgular-brak\"\u003e\u003ccode\u003e--min-severity\u003c/code\u003e: eşiğin altındaki bulguları bırak\u003c/h2\u003e\n\u003cp\u003eBelirtilen seviyenin altındaki önem derecesine sahip bulguları atmak için \u003ccode\u003e--min-severity \u0026lt;level\u0026gt;\u003c/code\u003e parametresini kullanın. Yalnızca eşik değerinde veya üzerindeki bulgular çıktıya ulaşır.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Yalnızca high ve critical bulguları göster\nleakwatch scan fs . --min-severity high\n\n# medium, high ve critical bulguları göster\nleakwatch scan fs . --min-severity medium\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003eoutput.severity-threshold\u003c/code\u003e altında yapılandırma dosyasında kalıcı bir varsayılan ayarlayabilirsiniz. \u003ccode\u003e--min-severity\u003c/code\u003e bayrağı, çalışma zamanında yapılandırma değerini geçersiz kılar:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003eoutput:\n severity-threshold: medium\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"--only-verified-yalnzca-onaylanm-aktif-srlar\"\u003e\u003ccode\u003e--only-verified\u003c/code\u003e: yalnızca onaylanmış aktif sırlar\u003c/h2\u003e\n\u003cp\u003eYalnızca doğrulama durumu \u003ccode\u003everified_active\u003c/code\u003e olan bulguları, yani Leakwatch'ın sağlayıcı API'sine kontrollü bir salt-okunur çağrı yaparak hâlâ geçerli olduğunu doğruladığı sırları tutmak için \u003ccode\u003e--only-verified\u003c/code\u003e parametresini kullanın. Diğer tüm bulgular (\u003ccode\u003eunverified\u003c/code\u003e, \u003ccode\u003everified_inactive\u003c/code\u003e veya \u003ccode\u003everify_error\u003c/code\u003e) bırakılır.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --only-verified\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBu bayrak, derlemeyi yalnızca onaylanmış olaylar üzerinde, yer tutucu veya zaten döndürülmüş kimlik bilgileri olabilecek şüpheli desenler üzerinde değil, başarısız kılmak istediğiniz CI hatlarında en kullanışlıdır.\u003c/p\u003e\n\u003cp\u003eHangi dedektörlerin canlı doğrulamayı desteklediği için \u003ca href=\"#/verification/how-verification-works\"\u003eDoğrulama Nasıl Çalışır\u003c/a\u003e bölümüne bakın.\u003c/p\u003e\n\u003ch2 id=\"filterexclude-detectors-belirli-dedektrleri-devre-d-brak\"\u003e\u003ccode\u003efilter.exclude-detectors\u003c/code\u003e: belirli dedektörleri devre dışı bırak\u003c/h2\u003e\n\u003cp\u003eBir veya daha fazla dedektörü kalıcı olarak devre dışı bırakmak için ID'lerini yapılandırma dosyasındaki \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e altında listeleyin. Listelenen dedektörlerden gelen bulgular, diğer ayarlardan bağımsız olarak hiçbir zaman üretilmez:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003efilter:\n exclude-detectors:\n - generic-api-key\n - jwt\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eDedektör ID'leri \u003ca href=\"#/detectors/detector-catalog\"\u003eDedektör Kataloğu\u003c/a\u003e'nda listelenmiştir. Bir dedektör sürekli olarak kod tabanınız için yanlış pozitifler ürettiğinde ve diğer bastırma mekanizmaları (satır içi yok saymalar veya \u003ccode\u003e.leakwatchignore\u003c/code\u003e) yeterince ayrıntılı olmadığında bu ayarı kullanın.\u003c/p\u003e\n\u003cp\u003eYapılandırma dosyasını düzenlemeden bir dedektörü tek bir çalıştırma için susturmak üzere, herhangi bir tarama alt komutunda \u003ccode\u003e--exclude-detectors \u0026lt;id\u0026gt;[,\u0026lt;id\u0026gt;...]\u003c/code\u003e (tekrarlanabilir) geçirin. Bu, \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e'ın yerine geçmek yerine ona ekleme yapar.\u003c/p\u003e\n\u003ch2 id=\"filterexclude-paths-tarama-ncesi-yollar-atla\"\u003e\u003ccode\u003efilter.exclude-paths\u003c/code\u003e: tarama öncesi yolları atla\u003c/h2\u003e\n\u003cp\u003eYolları tarama motoru okumadan önce dışlamak için yapılandırma dosyasında \u003ccode\u003efilter.exclude-paths\u003c/code\u003e kullanın. Desenler, \u003ccode\u003e.leakwatchignore\u003c/code\u003e ile aynı glob söz dizimini kullanır (standart globlar, \u003ccode\u003e**\u003c/code\u003e çift yıldız ve sondaki eğik çizgili dizin desenleri) ve \u003cstrong\u003etüm tarama kaynaklarına\u003c/strong\u003e uygulanır:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003efilter:\n exclude-paths:\n - \u0026quot;vendor/**\u0026quot;\n - \u0026quot;node_modules/**\u0026quot;\n - \u0026quot;**/*.min.js\u0026quot;\n - \u0026quot;**/*.min.css\u0026quot;\n - \u0026quot;test/fixtures/\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNot\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003e\u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e bayrağı, \u003ccode\u003efilter.exclude-paths\u003c/code\u003e'in komut satırı eşdeğeridir ve \u003ccode\u003escan slack\u003c/code\u003e \u003cstrong\u003ehariç\u003c/strong\u003e (bunun yerine \u003ccode\u003e--exclude-channels\u003c/code\u003e ile dışlama yapan) her tarama alt komutunda kullanılabilir. Tekrarlanabilir olup \u003ccode\u003efilter.exclude-paths\u003c/code\u003e'in yerine geçmek yerine ona ekleme yapar.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"cida-filtreleri-birletirme\"\u003eCI'da filtreleri birleştirme\u003c/h2\u003e\n\u003cp\u003eBir CI hattında genellikle yalnızca gerçek olaylarda başarısız olan, düşük gürültülü ve yüksek sinyalli bir çalışma istersiniz. Önerilen bir kombinasyon:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . \\\n --only-verified \\\n --min-severity high \\\n --format sarif \\\n --output results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eYapılandırma dosyasının kalıcı yol dışlamalarını yönetmesiyle:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003efilter:\n exclude-paths:\n - \u0026quot;vendor/**\u0026quot;\n - \u0026quot;node_modules/**\u0026quot;\n - \u0026quot;test/fixtures/\u0026quot;\n exclude-detectors:\n - generic-api-key\n\noutput:\n severity-threshold: high\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eArdından CI için yalnızca biçimi ve hedefi komut satırında geçersiz kılın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --only-verified --format sarif --output results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eDoğrulama ayrıntıları için \u003ca href=\"#/verification/how-verification-works\"\u003eDoğrulama Nasıl Çalışır\u003c/a\u003e, satır içi ve dosya tabanlı bastırma için \u003ca href=\"#/configuration/ignoring-findings\"\u003eBulguları Yok Sayma\u003c/a\u003e ve tam şema için \u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e bölümlerine bakın.\u003c/p\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/detectors/detector-catalog\"\u003eDedektör Kataloğu\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eDoğrulama Nasıl Çalışır\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/ignoring-findings\"\u003eBulguları Yok Sayma\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n"},"detectors/custom-rules":{"title":"Özel Kurallar","description":"YAML ile kendi sır tespit kalıplarınızı nasıl tanımlayacağınız ve 65 yerleşik dedektörün yanında bir Leakwatch taramasına nasıl ekleyeceğiniz.","html":"\u003ch1 id=\"zel-kurallar\"\u003eÖzel Kurallar\u003c/h1\u003e\n\u003cp\u003e65 yerleşik dedektör yaygın kullanılan kimlik bilgisi formatlarını kapsar; ancak her kuruluşun dahili token'ları, özel servis anahtarları veya hiçbir genel aracın önceden tahmin edemeyeceği ortama özgü kalıpları vardır. Özel kurallar, kaynak kodu değiştirmeden veya ikili dosyayı yeniden derlemeden kendi kalıplarınızı düz YAML ile tanımlamanıza ve çalışma zamanında yüklemenize olanak tanıyarak Leakwatch'ı genişletmenizi sağlar.\u003c/p\u003e\n\u003ch2 id=\"zel-kurallar-nerede-tanmlanr\"\u003eÖzel kurallar nerede tanımlanır\u003c/h2\u003e\n\u003cp\u003eÖzel kurallar, \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e yapılandırma dosyanızda en üst düzey bir \u003ccode\u003ecustom-rules:\u003c/code\u003e listesi altında tanımlanır:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003ecustom-rules:\n - id: acme-internal-token\n description: \u0026quot;ACME Corp dahili servis token'ı\u0026quot;\n regex: 'acme_[a-z0-9]{32}'\n keywords:\n - acme_\n severity: critical\n entropy: 3.5\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eKurallar, Leakwatch başladığında çalışma zamanında kaydedilir. Aynı Aho-Corasick ön-filtre hattını kullanarak yerleşik dedektörlerle birlikte çalışırlar.\u003c/p\u003e\n\u003ch2 id=\"kural-alanlar\"\u003eKural alanları\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eAlan\u003c/th\u003e\n\u003cth\u003eZorunlu\u003c/th\u003e\n\u003cth\u003eTür\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eid\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eEvet\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003eBenzersiz dedektör ID'si. Çıktıda ve \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e içinde kullanılır. Yerleşik dedektör ID'si veya başka bir özel kural ID'si ile çakışmamalıdır.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edescription\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHayır\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003eÇıktıda gösterilen insan tarafından okunabilir açıklama.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eregex\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eEvet\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003eRE2 uyumlu düzenli ifade. Maksimum 4096 karakter.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ekeywords\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHayır\u003c/td\u003e\n\u003ctd\u003estring listesi\u003c/td\u003e\n\u003ctd\u003eAho-Corasick ön-filtre anahtar kelimeleri. Regex yalnızca bu dizelerden en az birini içeren parçalar üzerinde çalışır. Bu alanın atlanması regex'in her parça üzerinde çalışmasına neden olur.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eseverity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHayır\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ecritical\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e veya \u003ccode\u003elow\u003c/code\u003e. Varsayılan \u003ccode\u003emedium\u003c/code\u003e'dur.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eentropy\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHayır\u003c/td\u003e\n\u003ctd\u003efloat\u003c/td\u003e\n\u003ctd\u003eShannon entropi eşiği (0–8). Entropisi bu değerin \u003cstrong\u003ealtında\u003c/strong\u003e olan eşleşmeler atılır. Düşük rastgelelikli yanlış pozitifleri filtrelemek için kullanışlıdır.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eİpucu\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eHer zaman \u003ccode\u003ekeywords\u003c/code\u003e belirtin. Tek kısa bir anahtar kelime bile (token ön eki gibi) regex motorunun işlediği parça sayısını önemli ölçüde azaltır ve büyük depolarda taramaların hızlı kalmasını sağlar. Örneğin tüm dahili token'larınız \u003ccode\u003eacme_\u003c/code\u003e ile başlıyorsa \u003ccode\u003ekeywords: [acme_]\u003c/code\u003e ayarlayın.\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eentropy\u003c/code\u003e kullanarak \u003ccode\u003eacme_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u003c/code\u003e gibi kalıbı karşılayan ancak açıkça gerçek sır olmayan yer tutucu değerlerdeki eşleşmeleri bastırın. 3,0–3,5 civarı bir eşik iyi bir başlangıç noktasıdır.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"akma-ynetimi\"\u003eÇakışma yönetimi\u003c/h2\u003e\n\u003cp\u003eBir özel kuralın \u003ccode\u003eid\u003c/code\u003e'si zaten kayıtlı bir dedektörle eşleşirse — yerleşik dedektör veya daha önce yüklenen özel kural olsun fark etmez — yinelenen kural \u003cstrong\u003eatlanır\u003c/strong\u003e ve bir hata loglanır. Leakwatch çökmez; geri kalan kurallar normal şekilde yüklenir. Bir özel kuralın etkisiz göründüğü durumlarda log çıktısını kontrol edin.\u003c/p\u003e\n\u003ch2 id=\"dorulama\"\u003eDoğrulama\u003c/h2\u003e\n\u003cp\u003eÖzel kuralların eşleştirilmiş doğrulayıcısı yoktur. Özel kurallardan gelen bulgular her zaman \u003ccode\u003eunverified\u003c/code\u003e durumuyla raporlanır — hiçbir zaman \u003ccode\u003everified_active\u003c/code\u003e veya \u003ccode\u003everified_inactive\u003c/code\u003e olmaz.\u003c/p\u003e\n\u003ch2 id=\"tam-rnek\"\u003eTam örnek\u003c/h2\u003e\n\u003cp\u003eAşağıdaki \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e, iki özel kural tanımlar: biri dahili servis token'ı, diğeri webhook'larda kullanılan imzalama sırrı için.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003ecustom-rules:\n - id: acme-internal-token\n description: \u0026quot;ACME Corp dahili servis token'ı (format: acme_ + 32 hex karakter)\u0026quot;\n regex: 'acme_[a-f0-9]{32}'\n keywords:\n - acme_\n severity: critical\n entropy: 3.2\n\n - id: acme-webhook-signing-secret\n description: \u0026quot;ACME Corp webhook imzalama sırrı (format: whsec_ + 40 base64url karakter)\u0026quot;\n regex: 'whsec_[A-Za-z0-9_\\-]{40}'\n keywords:\n - whsec_\n severity: high\n entropy: 3.5\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBu yapılandırmayla bir tarama çalıştırın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --config .leakwatch.yaml\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eÖzel kural bulgusu için örnek JSON çıktısı (sır değeri maskelenmiştir) — bir özel kural bulgusu, her yerleşik dedektörle tamamen aynı \u003ccode\u003eFinding\u003c/code\u003e şemasını kullanır (bkz. \u003ca href=\"#/output/output-formats\"\u003eÇıktı Biçimleri\u003c/a\u003e); özel kurallar için ayrı bir şema yoktur:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-json\"\u003e{\n \u0026quot;id\u0026quot;: \u0026quot;8a403a46336fc6ffdc847d202b74536c\u0026quot;,\n \u0026quot;detector_id\u0026quot;: \u0026quot;acme-internal-token\u0026quot;,\n \u0026quot;severity\u0026quot;: \u0026quot;critical\u0026quot;,\n \u0026quot;redacted\u0026quot;: \u0026quot;****cdef\u0026quot;,\n \u0026quot;source\u0026quot;: {\n \u0026quot;source_type\u0026quot;: \u0026quot;filesystem\u0026quot;,\n \u0026quot;file_path\u0026quot;: \u0026quot;config/production.env\u0026quot;,\n \u0026quot;line\u0026quot;: 14\n },\n \u0026quot;verification\u0026quot;: {\n \u0026quot;status\u0026quot;: \u0026quot;unverified\u0026quot;\n },\n \u0026quot;detected_at\u0026quot;: \u0026quot;2026-05-23T10:15:30Z\u0026quot;,\n \u0026quot;entropy\u0026quot;: 4.12\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNot\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003e\u003ccode\u003eredacted\u003c/code\u003e alanı gerçek sırrı her zaman maskeler (varsayılan olarak, \u003ccode\u003e****\u003c/code\u003e ve ardından en fazla son 4 karakter). Ham değer, açıkça \u003ccode\u003e--show-raw\u003c/code\u003e geçilmedikçe çıktıya asla yazılmaz (kontrollü ortamlar dışında önerilmez). Özel kuralların doğrulayıcısı olmadığından, \u003ccode\u003everification.status\u003c/code\u003e her zaman \u003ccode\u003eunverified\u003c/code\u003e olur.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"zel-kural-hari-tutma\"\u003eÖzel kuralı hariç tutma\u003c/h2\u003e\n\u003cp\u003eÖzel kurallar, yerleşik dedektörlerle aynı filtrelemeye katılır. Bir özel kuralı yapılandırmadan kaldırmadan devre dışı bırakmak için:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003efilter:\n exclude-detectors:\n - acme-internal-token\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eYapılandırma: Yapılandırma Dosyası\u003c/a\u003e — \u003ccode\u003ecustom-rules:\u003c/code\u003e öğesinin belge yapısındaki yeri dahil \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e için tam referans.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/detectors/detector-catalog\"\u003eDedektör Kataloğu\u003c/a\u003e — özel kuralınızı adlandırmadan önce ID çakışmalarını kontrol etmek için 65 yerleşik dedektör.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/how-it-works\"\u003eNasıl Çalışır\u003c/a\u003e — \u003ccode\u003ekeywords\u003c/code\u003e öğesinin bağlandığı Aho-Corasick ön-filtre hattı.\u003c/li\u003e\n\u003c/ul\u003e\n"},"detectors/detector-catalog":{"title":"Dedektör Kataloğu","description":"Kategorilere göre gruplanmış tüm 65 yerleşik dedektör; ID'leri, ne tespit ettikleri ve varsayılan önem dereceleri ile.","html":"\u003ch1 id=\"dedektr-katalou\"\u003eDedektör Kataloğu\u003c/h1\u003e\n\u003cp\u003eLeakwatch, bulut sağlayıcısı erişim anahtarlarından ve yapay zekâ API token'larından veritabanı bağlantı dizelerine ve özel kriptografik anahtarlara kadar geniş bir kimlik bilgisi türü yelpazesini kapsayan \u003cstrong\u003e65 yerleşik dedektör\u003c/strong\u003e ile gelir. Her dedektörün kararlı bir ID'si ve varsayılan önem derecesi vardır. Doğrulama kabiliyeti ayrıca doğrudan-canlı, bağlam-gerektiren, yalnız-format veya kullanılamaz olarak sınıflandırılır; kayıtlı bir doğrulayıcı tek başına canlı kapsamı kanıtlamaz.\u003c/p\u003e\n\u003cp\u003eBu sayfa her yerleşik dedektörü listeler. Doğrulama kapsamı ayrıntıları için \u003ca href=\"#/verification/verification-coverage\"\u003eDoğrulama Kapsamı\u003c/a\u003e bölümüne bakın. Kendi kalıplarınızı eklemek için \u003ca href=\"#/detectors/custom-rules\"\u003eÖzel Kurallar\u003c/a\u003e bölümüne bakın.\u003c/p\u003e\n\u003ch2 id=\"bu-katalogu-nasl-okuyacaksnz\"\u003eBu katalogu nasıl okuyacaksınız\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eID\u003c/strong\u003e — yapılandırma ve çıktıda kullanılan kararlı dize tanımlayıcısı. Bir dedektörü atlamak için \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e listesine ekleyin veya \u003ccode\u003e--min-severity\u003c/code\u003e filtrelemesiyle birlikte kullanın (\u003ca href=\"#/configuration/severity-and-filtering\"\u003eÖnem Derecesi \u0026amp; Filtreleme\u003c/a\u003e).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTespit eder\u003c/strong\u003e — dedektörün ne aradığı.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eÖnem Derecesi\u003c/strong\u003e — \u003ccode\u003eCritical\u003c/code\u003e (Kritik), \u003ccode\u003eHigh\u003c/code\u003e (Yüksek) veya \u003ccode\u003eMedium\u003c/code\u003e (Orta). Bu varsayılandır; \u003ccode\u003e--min-severity\u003c/code\u003e bayrağını ve \u003ccode\u003eoutput.severity-threshold\u003c/code\u003e yapılandırma anahtarını besler.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"bulut-ve-altyap\"\u003eBulut ve Altyapı\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eTespit eder\u003c/th\u003e\n\u003cth\u003eÖnem Derecesi\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eaws-access-key-id\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAWS Access Key ID\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egcp-service-account\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGCP Servis Hesabı Anahtarı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eazure-storage-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAzure Storage Bağlantı Dizesi\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eazure-entra-secret\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAzure Entra ID İstemci Sırrı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edigitalocean-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDigitalOcean Kişisel Erişim Token'ı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecloudflare-api-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCloudflare API Token'ı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eheroku-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHeroku API Anahtarı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003evercel-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eVercel API Token'ı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eterraform-cloud-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTerraform Cloud/Enterprise API Token'ı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ehashicorp-vault-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHashiCorp Vault Token'ı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edoppler-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDoppler Token — servis (\u003ccode\u003edp.st.\u003c/code\u003e), kişisel (\u003ccode\u003edp.pt.\u003c/code\u003e), CLI/yapılandırma (\u003ccode\u003edp.ct.\u003c/code\u003e) ve SCIM (\u003ccode\u003edp.scim.\u003c/code\u003e) token'ları\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"yapay-zek--makine-renimi\"\u003eYapay Zekâ / Makine Öğrenimi\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eTespit eder\u003c/th\u003e\n\u003cth\u003eÖnem Derecesi\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eopenai-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOpenAI API Anahtarı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eanthropic-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAnthropic API Anahtarı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edeepseek-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDeepSeek API Anahtarı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ehuggingface-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHugging Face API Token'ı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"demeler-ve-ticaret\"\u003eÖdemeler ve Ticaret\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eTespit eder\u003c/th\u003e\n\u003cth\u003eÖnem Derecesi\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003estripe-api-key-live\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eStripe Canlı API Anahtarı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003estripe-api-key-test\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eStripe Test API Anahtarı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecoinbase-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCoinbase API Anahtarı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eshopify-access-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eShopify Erişim Token'ı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"gelitirme-aralar-ci-ve-paketler\"\u003eGeliştirme Araçları, CI ve Paketler\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eTespit eder\u003c/th\u003e\n\u003cth\u003eÖnem Derecesi\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egithub-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGitHub Kişisel Erişim Token'ı — hem klasik \u003ccode\u003eghp_\u003c/code\u003e PAT hem de ince taneli \u003ccode\u003egithub_pat_\u003c/code\u003e PAT\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egithub-oauth-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGitHub OAuth2 ve kurulum (installation) token'ı — \u003ccode\u003egho_\u003c/code\u003e/\u003ccode\u003eghu_\u003c/code\u003e/\u003ccode\u003eghr_\u003c/code\u003e/\u003ccode\u003eghs_\u003c/code\u003e, yeni durumsuz (JWT biçimli) \u003ccode\u003eghs_\u003c/code\u003e kurulum token'ları dâhil\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egitlab-pat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGitLab kimlik bilgisi — klasik \u003ccode\u003eglpat-\u003c/code\u003e kişisel erişim token'ı ile dağıtım (\u003ccode\u003egldt-\u003c/code\u003e), runner (\u003ccode\u003eglrt-\u003c/code\u003e), CI/CD derleme ve tetikleyici (\u003ccode\u003eglcbt-\u003c/code\u003e/\u003ccode\u003eglptt-\u003c/code\u003e), OAuth uygulama sırrı (\u003ccode\u003egloas-\u003c/code\u003e) ve besleme (\u003ccode\u003eglft-\u003c/code\u003e) önekleri; repo URL'leri doğrulama yönlendirmesinde asla güvenilir sayılmaz\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ebitbucket-app-password\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBitbucket Uygulama Parolası\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecircleci-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCircleCI Kişisel API Token'ı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003enpm-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNPM Erişim Token'ı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epypi-api-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePyPI API Token'ı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003erubygems-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRubyGems API Anahtarı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edockerhub-pat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDocker Hub Kişisel Erişim Token'ı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esonarcloud-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSonarCloud/SonarQube Token'ı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esnyk-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSnyk API Anahtarı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edatabricks-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDatabricks Kişisel Erişim Token'ı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003elaunchdarkly-sdk-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eLaunchDarkly SDK Anahtarı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"letiim-ve-birlii\"\u003eİletişim ve İşbirliği\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eTespit eder\u003c/th\u003e\n\u003cth\u003eÖnem Derecesi\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eslack-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSlack Bot/Kullanıcı Token'ı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eslack-webhook\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSlack Webhook URL'si\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eteams-webhook\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMicrosoft Teams Gelen Webhook URL'si\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ediscord-bot-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDiscord Bot Token'ı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ediscord-webhook-url\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDiscord Webhook URL'si\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003etelegram-bot-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTelegram Bot Token'ı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003enotion-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNotion Dahili Entegrasyon Token'ı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003elinear-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eLinear API Anahtarı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003efigma-pat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFigma Kişisel Erişim Token'ı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eairtable-pat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAirtable Kişisel Erişim Token'ı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"e-posta-ve-mesajlama-teslimat\"\u003eE-posta ve Mesajlaşma Teslimatı\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eTespit eder\u003c/th\u003e\n\u003cth\u003eÖnem Derecesi\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esendgrid-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSendGrid API Anahtarı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003emailgun-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMailgun API Anahtarı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epostmark-server-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePostmark Sunucu API Token'ı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003etwilio-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYakınındaki \u003ccode\u003eSK...\u003c/code\u003e Key SID ile açıkça eşleştirilmiş Twilio API Key Secret; tek başına SID gizli değildir ve raporlanmaz\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"zleme-ve-gzlemlenebilirlik\"\u003eİzleme ve Gözlemlenebilirlik\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eTespit eder\u003c/th\u003e\n\u003cth\u003eÖnem Derecesi\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edatadog-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDatadog API Anahtarı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003enewrelic-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNew Relic API Anahtarı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egrafana-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGrafana API Anahtarı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esentry-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSentry Kimlik Doğrulama Token'ı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epagerduty-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePagerDuty API Anahtarı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"veritabanlar-ve-balant-dizeleri\"\u003eVeritabanları ve Bağlantı Dizeleri\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eTespit eder\u003c/th\u003e\n\u003cth\u003eÖnem Derecesi\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edatabase-connection-string\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eVeritabanı Bağlantı Dizesi\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eredis-connection-string\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRedis Bağlantı Dizesi\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003erabbitmq-connection-string\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRabbitMQ Bağlantı Dizesi\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esnowflake-credentials\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSnowflake Bağlantı Kimlik Bilgileri\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esupabase-service-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSupabase Kişisel Erişim Token'ı (\u003ccode\u003esbp_\u003c/code\u003e; kararlı eski dedektör ID'si)\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"kimlik-ve-eriim\"\u003eKimlik ve Erişim\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eTespit eder\u003c/th\u003e\n\u003cth\u003eÖnem Derecesi\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eauth0-management-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAuth0 Yönetim API Token'ı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eokta-api-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOkta API Token'ı\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eldap-credentials\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eLDAP/LDAPS Bağlama Kimlik Bilgileri\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"web3\"\u003eWeb3\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eTespit eder\u003c/th\u003e\n\u003cth\u003eÖnem Derecesi\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003einfura-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInfura API Anahtarı\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"genel-ve-kriptografik\"\u003eGenel ve Kriptografik\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eID\u003c/th\u003e\n\u003cth\u003eTespit eder\u003c/th\u003e\n\u003cth\u003eÖnem Derecesi\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egeneric-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGenel API Anahtarı\u003c/td\u003e\n\u003ctd\u003eMedium\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003estructured-config-secret\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eJSON/JSONC, yüksek güvenli YAML ve TOML skaler atamaları, XML öğe/öznitelikleri veya \u003ccode\u003e.env\u003c/code\u003e yapılandırmasında bağlamsal sır değeri\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ejwt\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eJSON Web Token\u003c/td\u003e\n\u003ctd\u003eHigh\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eprivate-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÖzel Anahtar (RSA, SSH, DSA, EC, PGP); PKCS8 \u003ccode\u003ePRIVATE KEY\u003c/code\u003e ve parola korumalı \u003ccode\u003eENCRYPTED PRIVATE KEY\u003c/code\u003e zarfı dahil\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eftp-credentials\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFTP/SFTP Kimlik Bilgileri\u003c/td\u003e\n\u003ctd\u003eCritical\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003chr\u003e\n\u003cp\u003e\u003cstrong\u003eToplam: 65 yerleşik dedektör.\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"nem-derecesine-gre-filtreleme\"\u003eÖnem derecesine göre filtreleme\u003c/h2\u003e\n\u003cp\u003eBulgular, komut satırında \u003ccode\u003e--min-severity\u003c/code\u003e veya yapılandırmada \u003ccode\u003eoutput.severity-threshold\u003c/code\u003e kullanılarak önem derecesine göre filtrelenebilir. Yalnızca belirtilen seviyede veya üzerindeki bulgular çıktıya dahil edilir. Ayrıntılar için \u003ca href=\"#/configuration/severity-and-filtering\"\u003eÖnem Derecesi \u0026amp; Filtreleme\u003c/a\u003e bölümüne bakın.\u003c/p\u003e\n\u003ch2 id=\"belirli-dedektrleri-hari-tutma\"\u003eBelirli dedektörleri hariç tutma\u003c/h2\u003e\n\u003cp\u003eBir veya daha fazla dedektörü tamamen atlamak için ID'lerini \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e içindeki \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e listesine ekleyin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003efilter:\n exclude-detectors:\n - generic-api-key\n - jwt\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eTam filtreleme referansı için \u003ca href=\"#/configuration/severity-and-filtering\"\u003eÖnem Derecesi \u0026amp; Filtreleme\u003c/a\u003e bölümüne bakın.\u003c/p\u003e\n\u003ch2 id=\"dorulama-kapsam\"\u003eDoğrulama kapsamı\u003c/h2\u003e\n\u003cp\u003eBazı dedektörlerin doğrudan canlı doğrulayıcısı vardır, bazıları güvenilir veya eşlik eden bağlam gerektirir, bazıları yalnızca format doğrular ve on birinin hiç doğrulayıcısı yoktur. Tam döküm için \u003ca href=\"#/verification/verification-coverage\"\u003eDoğrulama Kapsamı\u003c/a\u003e bölümüne bakın.\u003c/p\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/detectors/custom-rules\"\u003eÖzel Kurallar\u003c/a\u003e — YAML ile kendi tespit kalıplarınızı tanımlayın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/verification-coverage\"\u003eDoğrulama Kapsamı\u003c/a\u003e — hangi dedektörlerin canlı doğrulanabileceği.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/severity-and-filtering\"\u003eÖnem Derecesi \u0026amp; Filtreleme\u003c/a\u003e — bulguları önem derecesine veya dedektöre göre filtreleme.\u003c/li\u003e\n\u003c/ul\u003e\n"},"getting-started/how-it-works":{"title":"Nasıl Çalışır","description":"Leakwatch tarama hattının mimarisi: kaynaklar, tespit, doğrulama ve çıktı.","html":"\u003ch1 id=\"nasl-alr\"\u003eNasıl Çalışır\u003c/h1\u003e\n\u003cp\u003eLeakwatch hattını anlamak, performansı ayarlamanıza, sonuçları yorumlamanıza ve hangi bayrakları kullanacağınıza karar vermenize yardımcı olur. Bu sayfa, bir tarama komutunu çalıştırdığınız andan bir bulgunun çıktınızda göründüğü ana kadar neler olduğunu açıklar.\u003c/p\u003e\n\u003ch2 id=\"hatta-genel-bak\"\u003eHatta genel bakış\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-mermaid\"\u003eflowchart LR\n A([Kaynak\\nfs / git / image\\ns3 / gcs / slack]) --\u0026gt; B[İşçi Havuzu\\n—concurrency işçi]\n B --\u0026gt; C[Aho-Corasick\\nÖn-Filtre]\n C --\u0026gt; D[Regex\\nDedektörler]\n D --\u0026gt; E[Satır İçi İgnore\\nKontrolü]\n E --\u0026gt; F[Doğrulama\\nHavuzu\\n4 işçi / 10 rps]\n F --\u0026gt; G[Tarama Sonrası\\nFiltreler]\n G --\u0026gt; H([Çıktı\\njson / sarif\\ncsv / table\\ngithub])\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eHer aşama aşağıda ayrıntılı olarak açıklanmaktadır.\u003c/p\u003e\n\u003ch2 id=\"1-kaynak\"\u003e1. Kaynak\u003c/h2\u003e\n\u003cp\u003eHer tarama, motorun işlemesi için veri parçaları yayan bir soyutlama olan \u003cstrong\u003eKaynak\u003c/strong\u003e ile başlar. Leakwatch altı kaynak ile birlikte gelir:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eKaynak\u003c/th\u003e\n\u003cth\u003eKomut\u003c/th\u003e\n\u003cth\u003eNe yayar\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003eDosya sistemi\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003escan fs\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYerel bir dizin ağacındaki dosya içerikleri\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eGit geçmişi\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003escan git\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTüm commit geçmişindeki her blob\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eKonteyner imajı\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003escan image\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBir OCI/Docker imajının katman içerikleri, daemonsuz\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eAWS S3\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003escan s3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBir S3 kovasındaki nesne içerikleri\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eGoogle Cloud Storage\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003escan gcs\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBir GCS kovasındaki nesne içerikleri\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eSlack\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003escan slack\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eKanal/DM mesajları ve isteğe bağlı metin benzeri ekler\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNot\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eSlack varsayılan olarak hiçbir dosya indirmez. \u003ccode\u003e--include-files\u003c/code\u003e, metin benzeri eklerin boyutu sınırlı taramasını etkinleştirir ve Slack \u003ccode\u003efiles:read\u003c/code\u003e kapsamını gerektirir.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003cp\u003eParçalar, işçi havuzu tarafından tüketilen tamponlu bir kanala akar.\u003c/p\u003e\n\u003ch2 id=\"2-i-havuzu\"\u003e2. İşçi havuzu\u003c/h2\u003e\n\u003cp\u003eMotor, sabit sayıda \u003cstrong\u003egoroutine\u003c/strong\u003e içeren bir havuz yönetir — her biri \u003ccode\u003e--concurrency\u003c/code\u003e değerine karşılık gelir (varsayılan: CPU sayısı). Her işçi kanaldan bir parça alır ve tespit hattını bağımsız olarak çalıştırır. İşçiler değişebilir durum paylaşmadığından havuz, I/O ve bellek sınırlarına kadar eşzamanlılıkla doğrusal ölçeklenir.\u003c/p\u003e\n\u003cp\u003eTaramalar \u003ccode\u003eSIGINT\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e'e yanıt verir: iptal sinyali geldiğinde bağlam iptal edilir, işçiler mevcut parçalarını tamamlayıp durur ve kısmi sonuçlar çıktı yazılmadan önce toplanır.\u003c/p\u003e\n\u003ch2 id=\"3-aho-corasick-anahtar-kelime-n-filtresi\"\u003e3. Aho-Corasick anahtar kelime ön-filtresi\u003c/h2\u003e\n\u003cp\u003eHer parça üzerinde 65 dedektör desenini çalıştırmak yavaş olur. Bunun yerine motor, başlangıçta her dedektörün bildirdiği anahtar kelime listelerinden tek bir \u003cstrong\u003eAho-Corasick çok-desenli otomat\u003c/strong\u003e oluşturur. Her parça için bu otomat tek bir doğrusal geçiş yapar ve yalnızca anahtar kelimeleri parçanın baytlarında görünen dedektörleri döndürür.\u003c/p\u003e\n\u003cp\u003eBu, çoğu dedektörün çoğu parça üzerinde regex'ini hiç çalıştırmadığı anlamına gelir. Anahtar kelime bildirmeyen dedektörler her zaman çalışır (ön filtreyi atlayarak doğrudan regex'e geçerler).\u003c/p\u003e\n\u003cp\u003eAho-Corasick uygulaması \u003ca href=\"https://github.com/cloudflare/ahocorasick\"\u003ecloudflare/ahocorasick\u003c/a\u003e kütüphanesinden gelmektedir.\u003c/p\u003e\n\u003ch2 id=\"4-regex-dedektrler\"\u003e4. Regex dedektörler\u003c/h2\u003e\n\u003cp\u003eKısa listeye alınan her dedektör, derlenmiş \u003cstrong\u003edüzenli ifadesini\u003c/strong\u003e parça baytları üzerinde çalıştırır. Bir desen eşleştiğinde dedektör şunları içeren bir \u003ccode\u003eRawFinding\u003c/code\u003e döndürür:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eHam sır baytları (yalnızca doğrulama için bellekte tutulur; asla loglanmaz veya diske yazılmaz).\u003c/li\u003e\n\u003cli\u003eÇıktı için güvenli olan \u003cstrong\u003emaskelenmiş\u003c/strong\u003e bir gösterim.\u003c/li\u003e\n\u003cli\u003eİsteğe bağlı ek meta veri (örneğin bir AWS anahtarı için hesap kimliği).\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eLeakwatch, 61 paket genelinde \u003cstrong\u003e65 yerleşik dedektör\u003c/strong\u003e ile birlikte gelir; bulut sağlayıcılarını, yapay zekâ API'lerini, ödeme platformlarını, veritabanlarını, mesajlaşma araçlarını, sürüm kontrolünü, yapılandırılmış config dosyalarını ve daha fazlasını kapsar. \u003ca href=\"#/detectors/custom-rules\"\u003eÖzel YAML kuralları\u003c/a\u003e aracılığıyla kendi desenlerinizi ekleyebilirsiniz.\u003c/p\u003e\n\u003cp\u003eTüm dedektörler, Go'nun \u003ccode\u003einit()\u003c/code\u003e işlevi ve boş importlar kullanılarak derleme zamanında kaydedilir (ADR-0004). Çalışma zamanında eklenti yükleyici veya dinamik keşif yoktur.\u003c/p\u003e\n\u003ch2 id=\"5-satr-ii-ignore-kontrol\"\u003e5. Satır içi ignore kontrolü\u003c/h2\u003e\n\u003cp\u003eBir bulgu doğrulamaya gönderilmeden önce motor, kaynak satırın bir \u003cstrong\u003esatır içi ignore işareti\u003c/strong\u003e içerip içermediğini kontrol eder:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-go\"\u003e// leakwatch:ignore\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eveya dedektöre özgü bir varyant:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-go\"\u003e// leakwatch:ignore:aws-access-key-id\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eİşaret mevcutsa bulgu, \u003cstrong\u003eherhangi bir ağ çağrısı yapılmadan önce\u003c/strong\u003e sessizce bırakılır. Bu kasıtlıdır: yoksayılan sırlar asla canlı bir API isteğini tetiklememeli.\u003c/p\u003e\n\u003ch2 id=\"6-dorulama\"\u003e6. Doğrulama\u003c/h2\u003e\n\u003cp\u003eTüm parçalar için tespit tamamlandıktan sonra motor, bulguları ayrı bir \u003cstrong\u003edoğrulama işçi havuzuna\u003c/strong\u003e geçirir (varsayılan 4 işçi). Doğrulama:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eHer gerçek API çağrısını paylaşılan global ve dedektör başına birer \u003cstrong\u003ehız sınırlayıcıdan\u003c/strong\u003e geçirir (yapılandırılmış varsayılan değer her iki tavan için saniyede 10 istektir). Yalnız-format veya eksik-bağlam yolları token tüketmez; paylaşılan global kova toplam trafiği sınırlar ancak sağlayıcılar arasında adil zamanlama garantisi vermez.\u003c/li\u003e\n\u003cli\u003eSınırlı bölgesel fallback ve istek kabulü dahil olmak üzere her bulguya ait doğrulama operasyonuna \u003cstrong\u003ezaman aşımı\u003c/strong\u003e (varsayılan 10 saniye) uygular.\u003c/li\u003e\n\u003cli\u003eSağlayıcıya yalnızca \u003cstrong\u003esalt-okunur, yıkıcı olmayan\u003c/strong\u003e çağrılar yapar (örneğin AWS anahtarları için \u003ccode\u003ests:GetCallerIdentity\u003c/code\u003e).\u003c/li\u003e\n\u003cli\u003eHer bulguyu dört durumdan biriyle işaretler: \u003ccode\u003everified_active\u003c/code\u003e, \u003ccode\u003everified_inactive\u003c/code\u003e, \u003ccode\u003eunverified\u003c/code\u003e veya \u003ccode\u003everify_error\u003c/code\u003e.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eLeakwatch 65 dedektör türü için \u003cstrong\u003e54 doğrulayıcı implementasyonu\u003c/strong\u003e kaydeder (%83,1); ancak registry kaydı canlı kapsam değildir: 39'u doğrudan canlı, 9'u güvenilir issuer/bölge/eşlik eden bağlam gerektiren, 6'sı yalnız-format doğrulayıcısıdır. Kalan 11 türün doğrulayıcısı yoktur. Bağlam-gerektiren, yalnız-format ve doğrulayıcısız bulgular, belirtilen canlı doğrulama önkoşulları karşılanmadıkça \u003ccode\u003eunverified\u003c/code\u003e kalır.\u003c/p\u003e\n\u003cp\u003eBu aşamayı tamamen atlamak için \u003ccode\u003e--no-verify\u003c/code\u003e geçirin — hızlı, çevrimdışı taramalar için kullanışlıdır.\u003c/p\u003e\n\u003cp\u003eDoğrulama davranışı ve durum anlamları hakkında derinlemesine bilgi için \u003ca href=\"#/verification/how-verification-works\"\u003eDoğrulama Nasıl Çalışır\u003c/a\u003e sayfasına bakın.\u003c/p\u003e\n\u003ch2 id=\"7-bulgu-kimlii-ve-entropi\"\u003e7. Bulgu kimliği ve entropi\u003c/h2\u003e\n\u003cp\u003eHer bulgu, şu şekilde hesaplanan \u003cstrong\u003edeterministik bir kimlik\u003c/strong\u003e alır:\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003esha256(detectorID + redacted + filePath + decimal(line) + \u0026quot;:\u0026quot; + decimal(byteOffset))\n → ilk 16 bayta kısaltılır ve 32 karakterlik bir dizeye hex kodlanır\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eSonuç, düz, 32 karakterlik küçük harfli bir hex dizesidir (örn. \u003ccode\u003e447b5d2846d08ce25dd3d638cfe911ad\u003c/code\u003e) — \u003cstrong\u003eçizgili\u003c/strong\u003e bir UUID \u003cstrong\u003edeğildir\u003c/strong\u003e. Aynı konumdaki aynı sır her zaman aynı kimliği üretir; bu da bulguları çalıştırmalar arasında yinelenenleri kaldırmayı veya sorun izleyicilerde takip etmeyi güvenli kılar.\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003edetection.entropy.enabled\u003c/code\u003e doğru olduğunda \u003cstrong\u003eShannon entropisi\u003c/strong\u003e (aralık 0–8) boş olmayan her bulgu için hesaplanır ve bilgilendirme amacıyla çıktıda gösterilir; devre dışıyken yapay sıfır yerine alan bulunmaz. Motor düzeyinde, \u003ccode\u003edetection.entropy.threshold\u003c/code\u003e kapısı yalnızca açıkça buna dahil olan sezgisel dedektörlere uygulanır — şu anda yalnızca \u003ccode\u003egeneric-api-key\u003c/code\u003e — entropisi eşiğin altına düşen bir eşleşmeyi, düşük rastgelelikli yer tutucuları bastırmak için düşürür. \u003ccode\u003eaws-access-key-id\u003c/code\u003e veya \u003ccode\u003egithub-token\u003c/code\u003e gibi her yapısal (biçim-çapalı) dedektör entropi tarafından hiçbir zaman kapılanmaz: bu dedektörlerden gelen düşük entropili bir eşleşme yine de sonuçlarda görünür. Özel kurallar, bu motor düzeyindeki kapıdan ayrı olarak kendi bağımsız kural başına \u003ccode\u003eentropy\u003c/code\u003e eşiğini uygular (bkz. \u003ca href=\"#/detectors/custom-rules\"\u003eÖzel Kurallar\u003c/a\u003e).\u003c/p\u003e\n\u003ch2 id=\"8-tarama-sonras-filtreler\"\u003e8. Tarama sonrası filtreler\u003c/h2\u003e\n\u003cp\u003eDoğrulamadan sonra iki filtre uygulanır:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e--only-verified\u003c/code\u003e — \u003ccode\u003everified_active\u003c/code\u003e olmayan tüm bulguları bırakır.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e--min-severity\u003c/code\u003e — belirtilen önem düzeyinin (\u003ccode\u003elow\u003c/code\u003e | \u003ccode\u003emedium\u003c/code\u003e | \u003ccode\u003ehigh\u003c/code\u003e | \u003ccode\u003ecritical\u003c/code\u003e; varsayılan \u003ccode\u003elow\u003c/code\u003e) altındaki bulguları bırakır.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eHer iki filtre de doğrulama sonrasında çalışır; böylece \u003ccode\u003e--only-verified\u003c/code\u003e değerlendirildiğinde doğrulama durumu kullanılabilir olur.\u003c/p\u003e\n\u003ch2 id=\"9-kt\"\u003e9. Çıktı\u003c/h2\u003e\n\u003cp\u003eHayatta kalan bulgular beş \u003cstrong\u003ebiçimleyiciden\u003c/strong\u003e birine iletilir:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBiçim\u003c/th\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eYaygın kullanım\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003eJSON\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e--format json\u003c/code\u003e (varsayılan)\u003c/td\u003e\n\u003ctd\u003eMakine tarafından okunabilir, hat dostu\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eSARIF v2.1.0\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e--format sarif\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGitHub Code Scanning, güvenlik panoları\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eCSV\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e--format csv\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eElektronik tablolar, veri analizi\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eTablo\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e--format table\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTerminal incelemesi, önem derecesine göre renklendirilmiş\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eGitHub ek açıklamaları\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e--format github\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGitHub Actions'ta satır içi pull request ek açıklamaları\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eÇıktı varsayılan olarak stdout'a gider; bir dosyaya yazmak için \u003ccode\u003e--output \u0026lt;dosya\u0026gt;\u003c/code\u003e kullanın.\u003c/p\u003e\n\u003cp\u003eBiçim veya çıktı hedefi ne olursa olsun, her taramadan sonra bir \u003cstrong\u003etarama özeti\u003c/strong\u003e (tarih, kaynak türü, hedef, taranan dosyalar, süre, bulgu sayısı, kesme durumu) her zaman \u003cstrong\u003estderr\u003c/strong\u003e'e yazdırılır.\u003c/p\u003e\n\u003ch2 id=\"sr-gvenlii\"\u003eSır güvenliği\u003c/h2\u003e\n\u003cp\u003eLeakwatch, bulunan sırların doğrulama çağrıları dışında süreç sınırını asla terk etmemesi için tasarlanmıştır:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eHam sır baytları yalnızca tespit ve doğrulama sırasında bellekte yaşar.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e--show-raw\u003c/code\u003e bayrağı varsayılan olarak \u003ccode\u003efalse\u003c/code\u003e'tur; bu olmadan çıktıda yalnızca maskelenmiş gösterim görünür.\u003c/li\u003e\n\u003cli\u003eSırlar asla diske yazılmaz, \u003ccode\u003eslog\u003c/code\u003e aracılığıyla loglanmaz veya çalıştırmalar arasında önbelleğe alınmaz.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"tasarm-kararlar\"\u003eTasarım kararları\u003c/h2\u003e\n\u003cp\u003eMimari, ADR'ler olarak belgelenmiş çeşitli bilinçli seçimleri yansıtır:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eGo + CGO devre dışı\u003c/strong\u003e (ADR-0001) — tek statik ikili dosya, çalışma zamanı bağımlılığı yok, tüm platformlara çapraz derlenir.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCobra + Viper\u003c/strong\u003e (ADR-0002) — \u003ccode\u003ebayrak \u0026gt; env \u0026gt; yapılandırma \u0026gt; varsayılan\u003c/code\u003e önceliğiyle hiyerarşik CLI.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ego-git\u003c/strong\u003e (ADR-0003) — saf Go Git kütüphanesi; harici \u003ccode\u003egit\u003c/code\u003e ikili dosyası gerekmez.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDerleme zamanı dedektör kaydı\u003c/strong\u003e (ADR-0004) — \u003ccode\u003einit()\u003c/code\u003e + boş importlar; tür güvenli, çalışma zamanı eklenti yükleyicisi yok.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAho-Corasick hibrit eşleştirme\u003c/strong\u003e (ADR-0005) — ön filtre, alakasız parçalardaki regex çalışmasının çoğunu ortadan kaldırır.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ego-containerregistry\u003c/strong\u003e (ADR-0006) — daemonsuz katman analizi; imajları taramak için Docker daemon gerekmez.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eİşçi havuzu\u003c/strong\u003e (ADR-0008) — sabit goroutine sayısı, kanal tabanlı fan-out; öngörülebilir bellek ve CPU kullanımı.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eHızlı Başlangıç\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eDoğrulama Nasıl Çalışır\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Referansı\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/detectors/custom-rules\"\u003eÖzel Kurallar\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n"},"getting-started/installation":{"title":"Kurulum","description":"Leakwatch'ı Homebrew, go install, Docker veya hazır bir ikili dosya ile kurun.","html":"\u003ch1 id=\"kurulum\"\u003eKurulum\u003c/h1\u003e\n\u003cp\u003eLeakwatch'ı makinenize kurmak bir dakikadan az sürer. İş akışınıza en uygun yöntemi seçin: Homebrew macOS ve Linux'ta en basit seçenektir, \u003ccode\u003ego install\u003c/code\u003e halihazırda bir Go araç zinciriniz varsa idealdir, Docker ana sisteminizi temiz tutar ve hazır ikili dosyalar herhangi bir araç zinciri gerektirmeden her yerde çalışır.\u003c/p\u003e\n\u003ch2 id=\"homebrew-macos-ve-linux\"\u003eHomebrew (macOS ve Linux)\u003c/h2\u003e\n\u003cp\u003eResmi tap, amd64 ve arm64 mimarilerinde macOS ve Linux'u destekler.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003ebrew install HodeTech/tap/leakwatch\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eTap, \u003ca href=\"https://github.com/HodeTech/homebrew-tap\"\u003egithub.com/HodeTech/homebrew-tap\u003c/a\u003e adresinde barındırılmaktadır. Homebrew ile yükseltmek için \u003ccode\u003ebrew upgrade leakwatch\u003c/code\u003e komutunu kullanın.\u003c/p\u003e\n\u003ch2 id=\"go-install\"\u003ego install\u003c/h2\u003e\n\u003cp\u003eGo 1.25 veya daha yeni bir sürümü yüklüyse, en son sürümü doğrudan kaynaktan derleyip kurabilirsiniz:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003ego install github.com/HodeTech/leakwatch@latest\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eİkili dosya \u003ccode\u003e$(go env GOPATH)/bin\u003c/code\u003e dizinine yerleştirilir. Bu dizinin \u003ccode\u003ePATH\u003c/code\u003e değişkeninde olduğundan emin olun.\u003c/p\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNot\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003e\u003ccode\u003ego install\u003c/code\u003e her zaman en son etiketli sürümü getirir. Belirli bir sürüme sabitlemek için \u003ccode\u003e@latest\u003c/code\u003e yerine güncel sürüm etiketi olan \u003ccode\u003e@v1.8.0\u003c/code\u003e değerini kullanın.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"docker\"\u003eDocker\u003c/h2\u003e\n\u003cp\u003eMinimal, çok aşamalı bir Alpine imajı GitHub Container Registry'de yayımlanmaktadır. İmaj, root olmayan bir kullanıcı (\u003ccode\u003eleakwatch\u003c/code\u003e) olarak çalışır, CGO devre dışıdır ve çalışma dizini olarak \u003ccode\u003e/scan\u003c/code\u003e kullanır.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003edocker run --rm \\\n -v \u0026quot;$(pwd):/scan\u0026quot; \\\n ghcr.io/hodetech/leakwatch:latest \\\n scan fs /scan\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eKullanılabilir etiketler:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eEtiket\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e:latest\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eEn son sürüm\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e:v1.8.0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGüncel sürüme tam sabitleme\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e:v1.7\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGüncel küçük sürüme sabitleme (yama sürümlerini takip eder)\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eTaramak istediğiniz dizini konteyner içindeki \u003ccode\u003e/scan\u003c/code\u003e dizinine bağlayın. Bayraklar ve seçenekler yerel ikili dosyayla tamamen aynı şekilde çalışır — tam liste için \u003ca href=\"#/reference/cli-reference\"\u003eCLI Referansı\u003c/a\u003e sayfasına bakın.\u003c/p\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eİpucu\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eUzak Git depolarını tarama ve kimlik bilgilerini güvenli biçimde geçirme dahil Docker'a özgü kullanım kalıpları için \u003ca href=\"#/ci-cd/docker-usage\"\u003eDocker Kullanımı\u003c/a\u003e sayfasına bakın.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"hazr-ikili-dosya\"\u003eHazır ikili dosya\u003c/h2\u003e\n\u003cp\u003eHer sürüm, desteklenen tüm platformlar için \u003ca href=\"https://github.com/HodeTech/Leakwatch/releases\"\u003eGitHub Releases\u003c/a\u003e sayfasında tar arşivleri yayımlar. Platformunuza ait arşivi indirin, açın ve ikili dosyayı \u003ccode\u003ePATH\u003c/code\u003e değişkeninizdeki bir dizine taşıyın.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eDesteklenen platformlar:\u003c/strong\u003e amd64 ve arm64 mimarilerinde Linux, macOS ve Windows.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Linux amd64 örneği — OS ve ARCH değerlerini platformunuza göre değiştirin\ncurl -LO https://github.com/HodeTech/Leakwatch/releases/latest/download/leakwatch_Linux_amd64.tar.gz\ntar -xzf leakwatch_Linux_amd64.tar.gz\nsudo mv leakwatch /usr/local/bin/leakwatch\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003ePlatform adlandırması \u003ccode\u003eleakwatch_\u0026lt;OS\u0026gt;_\u0026lt;ARCH\u0026gt;.tar.gz\u003c/code\u003e kalıbını izler; \u003ccode\u003e\u0026lt;OS\u0026gt;\u003c/code\u003e değeri \u003ccode\u003eLinux\u003c/code\u003e, \u003ccode\u003eDarwin\u003c/code\u003e veya \u003ccode\u003eWindows\u003c/code\u003e, \u003ccode\u003e\u0026lt;ARCH\u0026gt;\u003c/code\u003e değeri ise \u003ccode\u003eamd64\u003c/code\u003e veya \u003ccode\u003earm64\u003c/code\u003e olabilir.\u003c/p\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eİpucu\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eHer sürüm ayrıca her arşiv için bir Yazılım Malzeme Listesi (SBOM) ve sürümün sağlama toplamları dosyası üzerinde bir \u003ca href=\"https://github.com/sigstore/cosign\"\u003eSigstore/cosign\u003c/a\u003e anahtarsız imza (\u003ccode\u003echecksums.txt.sig\u003c/code\u003e / \u003ccode\u003echecksums.txt.pem\u003c/code\u003e) yayımlar; böylece indirilen bir ikili dosyanın kökeni, tedarik zinciri açısından hassas ortamlarda doğrulanabilir.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"kurulumu-dorulama\"\u003eKurulumu doğrulama\u003c/h2\u003e\n\u003cp\u003eHerhangi bir kurulum yönteminin ardından ikili dosyanın erişilebilir olduğunu doğrulayın ve sürümü kontrol edin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch version\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBeklenen çıktı:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eleakwatch v1.8.0 (commit: 1a2b3c4, built: 2026-08-11T12:00:00Z)\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eKomut bulunamazsa kurulum dizininin \u003ccode\u003ePATH\u003c/code\u003e değişkeninde olup olmadığını kontrol edin.\u003c/p\u003e\n\u003ch2 id=\"sonraki-admlar\"\u003eSonraki adımlar\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eHızlı Başlangıç\u003c/a\u003e — ilk taramanızı bir dakikadan kısa sürede çalıştırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/how-it-works\"\u003eNasıl Çalışır\u003c/a\u003e — Leakwatch taramasının arkasındaki mimari.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e — \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e ile tarama davranışını özelleştirin.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eHızlı Başlangıç\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/docker-usage\"\u003eDocker Kullanımı\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Referansı\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n"},"getting-started/introduction":{"title":"Tanıtım","description":"Leakwatch nedir, neyi tarar ve sızan sırları nasıl tespit edip doğrular.","html":"\u003ch1 id=\"tantm\"\u003eTanıtım\u003c/h1\u003e\n\u003cp\u003e\u003cstrong\u003eLeakwatch\u003c/strong\u003e, sızan sırları — API anahtarları, token'lar, parolalar, bağlantı dizeleri ve özel anahtarlar — kod tabanlarınızda, Git geçmişinizde, konteyner imajlarınızda, bulut depolamanızda ve Slack çalışma alanlarınızda \u003cstrong\u003etespit eden, doğrulayan ve raporlayan\u003c/strong\u003e yüksek performanslı, açık kaynaklı (MIT) bir güvenlik aracıdır.\u003c/p\u003e\n\u003cp\u003eGo ile yazılmıştır, çalışma zamanı bağımlılığı olmayan tek bir statik ikili dosya olarak dağıtılır (\u003ccode\u003eCGO_ENABLED=0\u003c/code\u003e) ve her yerde çalışacak şekilde tasarlanmıştır: bir geliştirici dizüstü bilgisayarı, bir pre-commit kancası veya bir CI/CD hattı.\u003c/p\u003e\n\u003ch2 id=\"neden-leakwatch\"\u003eNeden Leakwatch\u003c/h2\u003e\n\u003cp\u003eTek bir commit'te sızan bir kimlik bilgisi — sonradan silinse bile — Git geçmişinde sonsuza dek erişilebilir kalabilir ve push edildikten dakikalar sonra istismar edilebilir. Leakwatch, bu sırları erken yakalamak ve hangilerinin \u003cem\u003egerçekten tehlikeli\u003c/em\u003e olduğunu söylemek için tasarlanmıştır:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eGeniş tespit\u003c/strong\u003e — bulut sağlayıcılarını, yapay zekâ API'lerini, ödeme platformlarını, veritabanlarını, mesajlaşma araçlarını, yapılandırılmış config dosyalarını ve daha fazlasını kapsayan 65 yerleşik dedektör; ayrıca kendi YAML özel kurallarınız.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDürüst doğrulama kabiliyeti\u003c/strong\u003e — 39 dedektör türü doğrudan canlı kontrole sahiptir, 9'u güvenilir issuer/bölge/eşlik eden bağlam gerektirir, 6'sı çevrimdışı format doğrular. Registry sayısı hiçbir zaman canlı kapsam gibi sunulmaz.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eÇok sayıda kaynak\u003c/strong\u003e — yerel dosya sistemi, eksiksiz bir Git geçmişi, bir OCI/Docker imajı, AWS S3, Google Cloud Storage ve Slack mesajları.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCI-uyumlu çıktı\u003c/strong\u003e — JSON, SARIF (GitHub Code Scanning için), CSV, renklendirilmiş bir terminal tablosu ve satır içi GitHub Actions ek açıklamaları.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTasarımı gereği sır-güvenli\u003c/strong\u003e — bulunan sırlar varsayılan olarak maskelenir ve asla loglanmaz, önbelleğe alınmaz veya diske yazılmaz.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"neleri-tarar\"\u003eNeleri tarar\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eKaynak\u003c/th\u003e\n\u003cth\u003eKomut\u003c/th\u003e\n\u003cth\u003eNeyi kapsar\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003eDosya sistemi\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eleakwatch scan fs\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYerel bir dizin ağacındaki dosyalar\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eGit geçmişi\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eleakwatch scan git\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTüm commit geçmişindeki her blob (yerel veya uzak)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eKonteyner imajı\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eleakwatch scan image\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOCI/Docker imaj katmanları, daemonsuz\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eAWS S3\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eleakwatch scan s3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBir S3 kovasındaki nesneler\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eGoogle Cloud Storage\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eleakwatch scan gcs\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBir GCS kovasındaki nesneler\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eSlack\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eleakwatch scan slack\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eKanallardaki ve (isteğe bağlı) DM'lerdeki mesaj metni\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eÇoklu depo\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eleakwatch scan repos\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAynı anda birden fazla Git deposu\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"tespit-ksaca-nasl-alr\"\u003eTespit kısaca nasıl çalışır\u003c/h2\u003e\n\u003cp\u003eLeakwatch, büyük girdilerde bile hızlı kalmak için katmanlı bir hat kullanır:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eAho-Corasick anahtar kelime ön-filtresi\u003c/strong\u003e — tek bir çok-desenli otomat, bir parçayı hangi dedektörlerin eşleştirebileceğine hızla karar verir; böylece dedektörlerin çoğu regex'ini hiç çalıştırmaz.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRegex doğrulaması\u003c/strong\u003e — yalnızca kısa listeye alınan dedektörler kesin desenlerini çalıştırır.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eEntropi\u003c/strong\u003e — Shannon entropisi gösterim için hesaplanır ve yerleşik \u003ccode\u003egeneric-api-key\u003c/code\u003e dedektörü ile kendi entropi eşiğine sahip her özel kuralı kapılayarak düşük rastgelelikteki yer tutucu eşleşmeleri eler. Diğer her (yapısal) yerleşik dedektör entropi tarafından hiçbir zaman kapılanmaz.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDoğrulama\u003c/strong\u003e — uygun bulgular canlı sağlayıcı API'sine karşı kontrol edilir.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eİpucu\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eLeakwatch'ı kullanmak için bu hattı anlamanız gerekmez — ancak taramaların neden hızlı olduğunu ve bazı bulguların neden bir doğrulama durumu gösterirken bazılarının göstermediğini açıklar. Tam tablo için \u003ca href=\"#/getting-started/how-it-works\"\u003eNasıl Çalışır\u003c/a\u003e bölümüne bakın.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"leakwatch-ne-deildir\"\u003eLeakwatch \u003cem\u003ene değildir\u003c/em\u003e\u003c/h2\u003e\n\u003cp\u003eBeklentileri doğru belirlemek için:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eGit geçmişini yeniden yazmaz veya sırları sizin için \u003cstrong\u003ekaldırmaz\u003c/strong\u003e — onları bulup raporlar ve (\u003ccode\u003e--remediation\u003c/code\u003e ile) nasıl döndüreceğinizi söyler.\u003c/li\u003e\n\u003cli\u003eSlack varsayılan olarak mesaj metnini tarar; boyutu sınırlı metin benzeri ekler \u003ccode\u003e--include-files\u003c/code\u003e ve \u003ccode\u003efiles:read\u003c/code\u003e kapsamıyla isteğe bağlıdır.\u003c/li\u003e\n\u003cli\u003eDoğrulama, birçok sır türü için mevcuttur ancak hepsi için değil — 11 dedektör türü (JWT'ler ve genel API anahtarları gibi) güvenli biçimde doğrulanamaz ve her zaman doğrulanmamış olarak raporlanır.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"sonraki-admlar\"\u003eSonraki adımlar\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/installation\"\u003eKurulum\u003c/a\u003e — Homebrew, \u003ccode\u003ego install\u003c/code\u003e, Docker veya hazır bir ikili dosya ile kurun.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eHızlı Başlangıç\u003c/a\u003e — ilk taramanızı bir dakikadan kısa sürede çalıştırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/how-it-works\"\u003eNasıl Çalışır\u003c/a\u003e — taramanın arkasındaki mimari.\u003c/li\u003e\n\u003c/ul\u003e\n"},"getting-started/quick-start":{"title":"Hızlı Başlangıç","description":"İlk Leakwatch taramanızı bir dakikadan kısa sürede çalıştırın.","html":"\u003ch1 id=\"hzl-balang\"\u003eHızlı Başlangıç\u003c/h1\u003e\n\u003cp\u003eLeakwatch'ın neler yapabileceğini anlamanın en hızlı yolu, onu gerçek bir dizine yönlendirmektir. Bu sayfa ilk taramanızda size rehberlik eder, çıktının ne anlama geldiğini açıklar ve en sık kullanacağınız bayrakları gösterir.\u003c/p\u003e\n\u003ch2 id=\"n-koullar\"\u003eÖn koşullar\u003c/h2\u003e\n\u003cp\u003eLeakwatch kurulu ve \u003ccode\u003ePATH\u003c/code\u003e değişkeninizde erişilebilir olmalıdır. Henüz yapmadıysanız \u003ca href=\"#/getting-started/installation\"\u003eKurulum\u003c/a\u003e sayfasına bakın.\u003c/p\u003e\n\u003ch2 id=\"lk-taramanz\"\u003eİlk taramanız\u003c/h2\u003e\n\u003cp\u003eMevcut dizini tek bir komutla tarayın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs .\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eVarsayılan olarak çıktı JSON biçiminde stdout'a yazılır. Bunun yerine okunabilir, renklendirilmiş bir tablo almak için \u003ccode\u003e--format table\u003c/code\u003e ekleyin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBir sonucun nasıl göründüğü aşağıdadır (tablo formatlayıcısı her zaman sondaki REMEDIATION sütununu yazdırır; \u003ccode\u003e--remediation\u003c/code\u003e de ayarlanmadıkça \u003ccode\u003e-\u003c/code\u003e gösterir):\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eSEVERITY DETECTOR FILE LINE REDACTED STATUS REMEDIATION\n-------- -------- ---- ---- -------- ------ -----------\nCRITICAL aws-access-key-id config/deploy.env 12 ****MNOP verified_active -\nHIGH npm-token scripts/bootstrap.sh 37 npm_****wxyz verified_active -\nMEDIUM generic-api-key src/services/analytics.js 89 ****w2y4 unverified -\n\nFound 3 secrets (1 critical, 1 high, 1 medium).\n\n── Scan Summary ─────────────────────────────────\n Date: 2026-05-23 14:03:11\n Source: filesystem\n Target: /home/user/myproject\n Files scanned: 312\n Duration: 1.24s\n Findings: 3\n─────────────────────────────────────────────────\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eTarama özeti her zaman \u003cstrong\u003estderr\u003c/strong\u003e'e yazdırılır; bu nedenle pipe veya yeniden yönlendirilen çıktıyla hiçbir zaman çakışmaz.\u003c/p\u003e\n\u003ch2 id=\"bulguyu-anlamak\"\u003eBulguyu anlamak\u003c/h2\u003e\n\u003cp\u003eTablodaki her satır (veya JSON'daki her nesne) bir bulguyu temsil eder. Temel alanlar şunlardır:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eAlan\u003c/th\u003e\n\u003cth\u003eAnlam\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eSEVERITY\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003eSır türünün ne kadar kritik olduğu: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e veya \u003ccode\u003ecritical\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eDETECTOR\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003eEşleşen dedektör — sır türünü tanımlar (örneğin \u003ccode\u003eaws-access-key-id\u003c/code\u003e)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eFILE\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003eSırrın bulunduğu dosyanın tarama köküne göreli yolu\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eLINE\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003eEşleşmenin satır numarası\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eREDACTED\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003eSırrın maskelenmiş gösterimi — \u003ccode\u003e--show-raw\u003c/code\u003e ayarlanmadıkça ham değer hiçbir zaman gösterilmez\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eSTATUS\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003eDoğrulama sonucu: \u003ccode\u003everified_active\u003c/code\u003e, \u003ccode\u003everified_inactive\u003c/code\u003e, \u003ccode\u003eunverified\u003c/code\u003e veya \u003ccode\u003everify_error\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eREMEDIATION\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003eDöndürme/iptal rehberi başlığı, veya \u003ccode\u003e--remediation\u003c/code\u003e geçilmediğinde \u003ccode\u003e-\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003ccode\u003everified_active\u003c/code\u003e durumu, Leakwatch'ın sözleşmeye uygun, yıkıcı olmayan bir sağlayıcı kontrolüyle sırrın hâlâ etkin olduğunu doğruladığı anlamına gelir. \u003cstrong\u003eHer \u003ccode\u003everified_active\u003c/code\u003e bulgusunu açık bir olay olarak değerlendirin.\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"yaygn-tarama-seenekleri\"\u003eYaygın tarama seçenekleri\u003c/h2\u003e\n\u003ch3 id=\"yalnzca-onaylanm-srlara-odaklann\"\u003eYalnızca onaylanmış sırlara odaklanın\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --only-verified\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBu seçenek doğrulanmamış ve etkin olmayan bulguları gizler; yalnızca etkin olduğu onaylananları bırakır. Çok sayıda sonucunuz olduğunda önceliklendirme için kullanışlıdır.\u003c/p\u003e\n\u003ch3 id=\"hzl-evrimd-tarama-iin-a-dorulamasn-atlayn\"\u003eHızlı çevrimdışı tarama için ağ doğrulamasını atlayın\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --no-verify\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eDoğrulama tamamen atlanır — hiçbir giden ağ çağrısı yapılmaz. Sonuçlar daha hızlı görünür ve internet bağlantısı olmadan çalışır, ancak tüm bulgular \u003ccode\u003eunverified\u003c/code\u003e olarak işaretlenir.\u003c/p\u003e\n\u003ch3 id=\"dzeltme-klavuzu-ekleyin\"\u003eDüzeltme kılavuzu ekleyin\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --remediation --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eHer bulgu, söz konusu sır türünü nasıl döndüreceğinizi veya iptal edeceğinizi açıklayan bir \u003cstrong\u003eREMEDIATION\u003c/strong\u003e sütunu kazanır. Bayrak ayarlandığında aynı veriler JSON, SARIF ve CSV çıktısına da dahil edilir.\u003c/p\u003e\n\u003ch3 id=\"minimum-nem-derecesine-gre-filtreleyin\"\u003eMinimum önem derecesine göre filtreleyin\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --min-severity high\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eYalnızca \u003ccode\u003ehigh\u003c/code\u003e veya \u003ccode\u003ecritical\u003c/code\u003e önem derecesindeki bulgular raporlanır.\u003c/p\u003e\n\u003ch3 id=\"sonular-dosyaya-kaydedin\"\u003eSonuçları dosyaya kaydedin\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --format sarif --output results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003e--output\u003c/code\u003e / \u003ccode\u003e-o\u003c/code\u003e bayrağı stdout yerine bir dosyaya yazar. SARIF çıktısı \u003ca href=\"https://docs.github.com/en/code-security/code-scanning\"\u003eGitHub Code Scanning\u003c/a\u003e ile uyumludur.\u003c/p\u003e\n\u003ch2 id=\"yaplandrma-dosyas-oluturma\"\u003eYapılandırma dosyası oluşturma\u003c/h2\u003e\n\u003cp\u003eİlk denemede varsayılanlarla çalıştırmak uygundur; ancak tekrarlayan kullanım için proje düzeyinde bir yapılandırma isteyeceksiniz:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch init\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBu komut, eşzamanlılık, entropi, doğrulama, çıktı biçimi ve yaygın yol dışlamaları için önerilen varsayılanlarla mevcut dizine \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e yazar. Mevcut bir dosyanın üzerine yazmak için \u003ccode\u003e--force\u003c/code\u003e, farklı bir yola yazmak için \u003ccode\u003e--output\u003c/code\u003e kullanın.\u003c/p\u003e\n\u003cp\u003eHer seçeneğin tam açıklaması için \u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e sayfasına bakın.\u003c/p\u003e\n\u003ch2 id=\"k-kodlar\"\u003eÇıkış kodları\u003c/h2\u003e\n\u003cp\u003eLeakwatch, CI betiklerinin çıktıyı ayrıştırmadan sonuçlara göre hareket edebilmesi için farklı çıkış kodları kullanır:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eKod\u003c/th\u003e\n\u003cth\u003eAnlam\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlandı — bulgu yok\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlandı — bir veya daha fazla sır bulundu\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama bir hata nedeniyle başarısız oldu\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlanmadan (\u003ccode\u003eCtrl+C\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e) kesildi ve henüz hiçbir bulgu raporlanmamıştı\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eTipik bir CI kapısı şöyle görünür:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --only-verified --format sarif --output results.sarif\nif [ $? -eq 1 ]; then\n echo \u0026quot;Etkin sırlar bulundu — derleme başarısız\u0026quot;\n exit 1\nfi\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-warn\"\u003e\u003cdiv class=\"callout-label\"\u003eUyarı\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eÇıkış kodu \u003ccode\u003e1\u003c/code\u003e, etkin filtreleri geçen (\u003ccode\u003e--min-severity\u003c/code\u003e ve \u003ccode\u003e--only-verified\u003c/code\u003e dahil) \u003cem\u003eherhangi bir\u003c/em\u003e bulgu olduğunda döndürülür. Temiz çıkış kodu \u003ccode\u003e0\u003c/code\u003e, hiçbir bulgunun eşleşmediği anlamına gelir — kod tabanında sır olmadığı anlamına gelmez.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"taramay-iptal-etme\"\u003eTaramayı iptal etme\u003c/h2\u003e\n\u003cp\u003eÇalışan bir taramayı iptal etmek için \u003ccode\u003eCtrl+C\u003c/code\u003e tuşuna basın (veya \u003ccode\u003eSIGTERM\u003c/code\u003e gönderin). Leakwatch düzgün biçimde durur: işlemdeki parçalar tamamlanır, kısmi sonuçlar yazılır ve özet \u003ccode\u003eStatus: interrupted (partial results)\u003c/code\u003e olarak gösterilir. Bulgular kesintiye rağmen önceliklidir — kesintiden önce sırlar zaten bulunmuşsa tarama \u003ccode\u003e1\u003c/code\u003e ile çıkar, böylece bu sinyal hiçbir zaman gizlenmez. \u003ccode\u003e3\u003c/code\u003e çıkış kodu yalnızca tarama sıfır bulguyla kesintiye uğradığında döndürülür; bu sayede bir CI hattı tamamlanmamış bir taramayı asla temiz bir geçişle karıştırmaz.\u003c/p\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/installation\"\u003eKurulum\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/how-it-works\"\u003eNasıl Çalışır\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Referansı\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eDoğrulama Nasıl Çalışır\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n"},"output/output-formats":{"title":"Çıktı Formatları","description":"Leakwatch'ın desteklediği beş çıktı formatı — JSON, SARIF, CSV, tablo ve GitHub ek açıklamaları — örnekler ve her birini ne zaman kullanacağınıza dair rehberlik.","html":"\u003ch1 id=\"kt-formatlar\"\u003eÇıktı Formatları\u003c/h1\u003e\n\u003cp\u003eLeakwatch beş çıktı formatını destekler: makine tarafından okunabilir hatlar, güvenlik araç entegrasyonları, elektronik tablo dışa aktarmaları, insan tarafından okunabilir terminal incelemesi ve GitHub Actions ek açıklamaları. \u003ccode\u003e--format\u003c/code\u003e (veya \u003ccode\u003e-f\u003c/code\u003e) ile bir format seçin; stdout yerine bir dosyaya yazmak için \u003ccode\u003e--output\u003c/code\u003e (veya \u003ccode\u003e-o\u003c/code\u003e) kullanın.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --format json\nleakwatch scan fs . --format sarif --output results.sarif\nleakwatch scan fs . --format csv --output findings.csv\nleakwatch scan fs . --format table\nleakwatch scan fs . --format github # GitHub Actions ek açıklamaları (CI kullanımı)\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eVarsayılan format \u003ccode\u003ejson\u003c/code\u003e'dur.\u003c/p\u003e\n\u003ch2 id=\"json\"\u003eJSON\u003c/h2\u003e\n\u003cp\u003eJSON varsayılan format ve en eksiksiz temsil biçimidir. Leakwatch, stdout'a (veya \u003ccode\u003e--output\u003c/code\u003e ile verilen dosyaya) bulgu nesnelerinden oluşan bir JSON \u003cstrong\u003edizisi\u003c/strong\u003e yazar.\u003c/p\u003e\n\u003cp\u003eHam sır değeri, \u003ccode\u003e--show-raw\u003c/code\u003e açıkça ayarlanmadıkça \u003cstrong\u003ehiçbir zaman\u003c/strong\u003e serileştirilmez. \u003ccode\u003e--show-raw\u003c/code\u003e ile her nesneye bir \u003ccode\u003e\u0026quot;raw\u0026quot;\u003c/code\u003e alanı eklenir.\u003c/p\u003e\n\u003ch3 id=\"rnek-ar\"\u003eÖrnek çağrı\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs ./src --format json --output findings.json\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"rnek-bulgu-nesnesi\"\u003eÖrnek bulgu nesnesi\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-json\"\u003e{\n \u0026quot;id\u0026quot;: \u0026quot;447b5d2846d08ce25dd3d638cfe911ad\u0026quot;,\n \u0026quot;detector_id\u0026quot;: \u0026quot;github-token\u0026quot;,\n \u0026quot;severity\u0026quot;: \u0026quot;critical\u0026quot;,\n \u0026quot;redacted\u0026quot;: \u0026quot;ghp_****************************Xk9R\u0026quot;,\n \u0026quot;source\u0026quot;: {\n \u0026quot;source_type\u0026quot;: \u0026quot;filesystem\u0026quot;,\n \u0026quot;file_path\u0026quot;: \u0026quot;scripts/deploy.sh\u0026quot;,\n \u0026quot;line\u0026quot;: 14\n },\n \u0026quot;verification\u0026quot;: {\n \u0026quot;status\u0026quot;: \u0026quot;verified_active\u0026quot;\n },\n \u0026quot;entropy\u0026quot;: 5.82,\n \u0026quot;detected_at\u0026quot;: \u0026quot;2026-05-23T10:15:30Z\u0026quot;\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003eid\u003c/code\u003e, düz 32 karakterlik küçük harfli bir hex dizesi olarak gösterilen, deterministik ve kısaltılmış bir SHA-256 değeridir — bir UUID \u003cstrong\u003edeğildir\u003c/strong\u003e ve hiçbir zaman tire içermez. Nasıl hesaplandığı için \u003ca href=\"#/getting-started/how-it-works\"\u003eNasıl Çalışır\u003c/a\u003e sayfasına bakın.\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eentropy\u003c/code\u003e hesaplanmadığında atlanır; hesaplanan \u003ccode\u003e0.0\u003c/code\u003e değeri sıfır olarak yazılır. Zaman damgası olmayan kütüphane üretimi bulgularda \u003ccode\u003edetected_at\u003c/code\u003e, Go'nun 1. yıl sıfır zamanı yerine atlanır. Normal CLI bulguları zaman damgasını ayarlar.\u003c/p\u003e\n\u003cp\u003eSağlayıcı tarafından doğrulanmış kimlik, kapsam ve sona erme meta verisi mevcut olduğunda \u003ccode\u003everification.extra_data\u003c/code\u003e altında görünür. Değerler sınırlı ve şema doğrulamalıdır; alanın olmaması token'ın izni veya sona ermesi olmadığı değil, sağlayıcının güvenli meta veri sunmadığı anlamına gelir.\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003e--remediation\u003c/code\u003e de ayarlandığında her bulgunun içine iç içe bir \u003ccode\u003e\u0026quot;remediation\u0026quot;\u003c/code\u003e nesnesi yerleştirilir. Bkz. \u003ca href=\"#/output/remediation\"\u003eDüzeltme Rehberi\u003c/a\u003e.\u003c/p\u003e\n\u003ch2 id=\"sarif\"\u003eSARIF\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003esarif\u003c/code\u003e formatı, \u003ca href=\"https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/uploading-a-sarif-file-to-github\"\u003eGitHub Code Scanning\u003c/a\u003e'e yüklenmek üzere tasarlanmış bir SARIF v2.1.0 belgesi üretir. Araç adı \u003ccode\u003eLeakwatch\u003c/code\u003e'tır ve \u003ccode\u003einformationUri\u003c/code\u003e \u003ccode\u003ehttps://github.com/HodeTech/Leakwatch\u003c/code\u003e adresine işaret eder.\u003c/p\u003e\n\u003cp\u003eBulgularda görünen her dedektör, SARIF sürücüsünde bir \u003cstrong\u003ekural\u003c/strong\u003e haline gelir; \u003ccode\u003e--remediation\u003c/code\u003e ayarlandığında düzeltme adımlarından doldurulan \u003ccode\u003ehelp\u003c/code\u003e metni ve sağlayıcı belgelerine işaret eden bir \u003ccode\u003ehelpUri\u003c/code\u003e ile birlikte. Sonuçlar, dedektör ID'si, maskelenmiş değer ve dosya yolundan hesaplanan bir \u003ccode\u003eleakwatch/v1\u003c/code\u003e kısmi parmak izi taşır — bu, çevresindeki kod kaydığında bile GitHub Code Scanning'in aynı uyarıyı takip etmesini sağlar.\u003c/p\u003e\n\u003ch3 id=\"rnek-ar-1\"\u003eÖrnek çağrı\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --format sarif --output results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"github-code-scanninge-ykleme\"\u003eGitHub Code Scanning'e yükleme\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003e# Bir GitHub Actions iş akışı adımında:\n- name: SARIF sonuçlarını yükle\n uses: github/codeql-action/upload-sarif@v3\n with:\n sarif_file: results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eTam CI kurulumu için \u003ca href=\"#/ci-cd/github-action\"\u003eGitHub Action\u003c/a\u003e bölümüne bakın.\u003c/p\u003e\n\u003ch2 id=\"csv\"\u003eCSV\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003ecsv\u003c/code\u003e formatı, bir başlık satırı ve ardından bulgu başına bir satır yazar; standart virgülle ayrılmış değerler kullanır. Her hücre yazılmadan önce elektronik tablo formül enjeksiyonuna karşı sterilize edilir.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSütunlar (varsayılan):\u003c/strong\u003e\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eid,detector_id,severity,redacted,source,file_path,line,commit,verification_status,remediation\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003esource\u003c/code\u003e, bir bulgunun geldiği kaynak depoyu, konteyner imajını veya Slack kanalını tanımlayan, insan tarafından okunabilir bir etikettir (bir \u003ccode\u003escan repos\u003c/code\u003e çalıştırması birden fazla depodan gelen bulguları tek bir CSV'de birleştirdiğinde kullanışlıdır); tek bir dizinin düz bir dosya sistemi taraması için boştur. \u003ccode\u003eline\u003c/code\u003e satır numarasıdır, uygulanamadığında boştur.\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003e--show-raw\u003c/code\u003e ayarlandığında, sona bir \u003ccode\u003eraw\u003c/code\u003e sütunu eklenir.\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eremediation\u003c/code\u003e sütunu, \u003ccode\u003e--remediation\u003c/code\u003e ayarlandığında düzeltme başlığını (örn. \u003ccode\u003e\u0026quot;Revoke GitHub Token\u0026quot;\u003c/code\u003e) içerir, aksi hâlde boş kalır.\u003c/p\u003e\n\u003ch3 id=\"rnek-ar-2\"\u003eÖrnek çağrı\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git . --format csv --output findings.csv\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"rnek-kt\"\u003eÖrnek çıktı\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-csv\"\u003eid,detector_id,severity,redacted,source,file_path,line,commit,verification_status,remediation\n447b5d2846d08ce25dd3d638cfe911ad,github-token,critical,ghp_****Xk9R,,scripts/deploy.sh,14,7d3e1f2,verified_active,Revoke GitHub Token\ne6fa909746d7d5242309b64c33209fa9,aws-access-key-id,high,AKIA****K7NP,,config/aws.yml,3,7d3e1f2,unverified,Rotate AWS Access Key\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"tablo\"\u003eTablo\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003etable\u003c/code\u003e formatı; CJK, birleşen işaretler ve emoji için doğru görüntü genişliği dahil, terminal hücrelerine göre hizalanmış insan tarafından okunabilir bir tablo yazar. Sonuçların hızlı görsel taramasını istediğiniz etkileşimli terminal oturumları için en uygun formattır.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSütunlar:\u003c/strong\u003e\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eSEVERITY | DETECTOR | FILE | LINE | REDACTED | STATUS | REMEDIATION\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003eLINE\u003c/code\u003e sütunu, satır numarası mevcut olmadığında (örneğin bir Slack veya konteyner imajı bulgusu için) \u003ccode\u003e-\u003c/code\u003e gösterir. \u003ccode\u003eREMEDIATION\u003c/code\u003e sütunu her zaman bulunur ve \u003ccode\u003e--remediation\u003c/code\u003e ayarlanmadıkça \u003ccode\u003e-\u003c/code\u003e gösterir. \u003ccode\u003e--show-raw\u003c/code\u003e ayarlandığında da, sona bir \u003ccode\u003eRAW\u003c/code\u003e sütunu eklenir. Yalnızca tablo çıktısında bu değer geri çevrilebilir, Go-tırnaklı bir dize olarak gösterilir: \u003ccode\u003e\\\\n\u003c/code\u003e, \u003ccode\u003e\\\\t\u003c/code\u003e ve \u003ccode\u003e\\\\x1b\u003c/code\u003e gibi kaçışlar terminal kontrol baytlarını etkisiz tutarken \u003ccode\u003estrconv.Unquote\u003c/code\u003e özgün baytları eksiksiz geri yükleyebilir. JSON ve CSV ham değerleri mevcut makine-okunur gösterimlerini korur. Tablonun altına bir özet satırı yazdırılır (örn. \u003ccode\u003eFound 3 secrets (1 critical, 2 high).\u003c/code\u003e).\u003c/p\u003e\n\u003cp\u003eSaldırgan tarafından etkilenebilecek alanlar (dedektör ID'si, dosya yolu, maskelenmiş değer), terminale yazılmadan önce kontrol karakterlerinden ve ANSI kaçış dizilerinden arındırılır. Unicode çift yönlü metin (bidi) kontrol karakterleri görünür \u003ccode\u003e\\\\uXXXX\u003c/code\u003e kod noktaları olarak yazılır; böylece varlıkları gizlenmeden görsel sıralama saldırıları önlenir. CJK, birleşen işaretler ve emoji ZWJ dizileri dahil olağan uluslararası metin korunur.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eANSI rengi\u003c/strong\u003e, \u003ccode\u003eSEVERITY\u003c/code\u003e sütununa otomatik olarak uygulanır, ancak yalnızca dört koşulun tamamı sağlandığında:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003ccode\u003e--format table\u003c/code\u003e seçilmiş\u003c/li\u003e\n\u003cli\u003eÇıktı stdout'a gidiyor (\u003ccode\u003e--output \u0026lt;file\u0026gt;\u003c/code\u003e yok)\u003c/li\u003e\n\u003cli\u003estdout bir TTY (pipe veya yönlendirme değil)\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eNO_COLOR\u003c/code\u003e ortam değişkeni ayarlanmamış\u003c/li\u003e\n\u003c/ol\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eÖnem derecesi\u003c/th\u003e\n\u003cth\u003eRenk\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecritical\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eKalın kırmızı\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ehigh\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eKırmızı\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003emedium\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSarı\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMavi\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"rnek-ar-3\"\u003eÖrnek çağrı\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --format table --min-severity high\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"rnek-kt-1\"\u003eÖrnek çıktı\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eSEVERITY DETECTOR FILE LINE REDACTED STATUS REMEDIATION\n-------- -------- ---- ---- -------- ------ -----------\nCRITICAL github-token scripts/deploy.sh 14 ghp_****Xk9R verified_active Revoke GitHub Token\nHIGH aws-access-key-id config/aws.yml 3 AKIA****K7NP unverified Rotate AWS Access Key\n\nFound 2 secrets (1 critical, 1 high).\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"github-ek-aklamalar\"\u003eGitHub ek açıklamaları\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003egithub\u003c/code\u003e formatı, \u003ca href=\"https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions\"\u003eGitHub Actions iş akışı komutlarını\u003c/a\u003e (\u003ccode\u003e::error\u003c/code\u003e / \u003ccode\u003e::warning\u003c/code\u003e / \u003ccode\u003e::notice\u003c/code\u003e) yayar; böylece bulgular bir pull request'in \u003cem\u003eFiles changed\u003c/em\u003e görünümünde ve çalışma günlüğünde \u003cstrong\u003esatır içi ek açıklamalar\u003c/strong\u003e olarak görünür. Runner'ın stdout'una akıtılmak üzere tasarlanmıştır — bir dosyaya yazmanın etkisi yoktur.\u003c/p\u003e\n\u003cp\u003eÖnem derecesi ek açıklama seviyesine eşlenir: \u003ccode\u003ecritical\u003c/code\u003e → \u003ccode\u003eerror\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e → \u003ccode\u003ewarning\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e/\u003ccode\u003elow\u003c/code\u003e → \u003ccode\u003enotice\u003c/code\u003e. Dosya yolu olan bir bulgu o dosya ve satıra bağlanır; dosya yolu olmayan bir bulgu çalışma düzeyinde (run-level) bir ek açıklama olur.\u003c/p\u003e\n\u003cp\u003eGüvenlik için bu format ham sırrı \u003cstrong\u003easla\u003c/strong\u003e yazdırmaz — \u003ccode\u003e--show-raw\u003c/code\u003e ile bile yalnızca maskelenmiş değer gösterilir; çünkü ek açıklamalar (çoğu zaman herkese açık olan) PR arayüzünde ve günlüklerde görüntülenir.\u003c/p\u003e\n\u003ch3 id=\"rnek-ar-4\"\u003eÖrnek çağrı\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --format github\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"rnek-kt-2\"\u003eÖrnek çıktı\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003e::error file=config/prod.env,line=12,title=Leakwatch%3A aws-access-key-id::Potential secret detected by aws-access-key-id (critical): AKIA****K7NP\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBu format normalde elle çağrılmak yerine \u003ca href=\"#/ci-cd/github-action\"\u003eGitHub Action\u003c/a\u003e (\u003ccode\u003eformat: github\u003c/code\u003e) tarafından kullanılır.\u003c/p\u003e\n\u003ch2 id=\"yaygn-kt-bayraklar\"\u003eYaygın çıktı bayrakları\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eKısa\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-f\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktı formatı: \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, \u003ccode\u003egithub\u003c/code\u003e (varsayılan \u003ccode\u003ejson\u003c/code\u003e)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-o\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estdout yerine dosyaya yaz\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--show-raw\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktıya maskelenmemiş sır değerini dahil et\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003c/td\u003e\n\u003ctd\u003eBu önem seviyesinin altındaki bulguları bırak\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003c/td\u003e\n\u003ctd\u003eYalnızca \u003ccode\u003everified_active\u003c/code\u003e bulgularını tut\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--remediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003c/td\u003e\n\u003ctd\u003eBulguları sağlayıcı düzeltme rehberiyle zenginleştir\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/output/remediation\"\u003eDüzeltme Rehberi\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/github-action\"\u003eGitHub Action\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eDoğrulama Nasıl Çalışır\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n"},"output/remediation":{"title":"Düzeltme Rehberi","description":"Bulguları sağlayıcıya özgü döndürme ve iptal adımları, aciliyet dereceleri ve resmi dokümantasyon bağlantılarıyla zenginleştirmek için --remediation kullanın.","html":"\u003ch1 id=\"dzeltme-rehberi\"\u003eDüzeltme Rehberi\u003c/h1\u003e\n\u003cp\u003eBir sırrın sızdığını bilmek işin yalnızca yarısıdır — ayrıca ne yapacağınızı da bilmeniz gerekir. Herhangi bir tarama komutuna \u003ccode\u003e--remediation\u003c/code\u003e eklemek, her bulguyu yapılandırılmış, sağlayıcıya özgü rehberlikle zenginleştirir: kimlik bilgisini döndürme veya iptal etme adımları, sağlayıcının belgelerine bağlantı, yönetim konsoluna bağlantı, aciliyet derecelendirmesi ve bir doğrulama kontrol listesi.\u003c/p\u003e\n\u003ch2 id=\"nasl-etkinletirilir\"\u003eNasıl etkinleştirilir\u003c/h2\u003e\n\u003cp\u003eHerhangi bir tarama komutuna \u003ccode\u003e--remediation\u003c/code\u003e ekleyin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --remediation\nleakwatch scan git . --remediation --format json\nleakwatch scan image myapp:latest --remediation --format sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eDüzeltme zenginleştirmesi varsayılan olarak devre dışıdır. Bayrak yoksa, her bulgunun \u003ccode\u003eremediation\u003c/code\u003e alanı \u003ccode\u003enull\u003c/code\u003e olur ve fazladan veri alınmaz veya hesaplanmaz.\u003c/p\u003e\n\u003ch2 id=\"ne-ierir\"\u003eNe içerir\u003c/h2\u003e\n\u003cp\u003eHer düzeltme girişi aşağıdaki alanları içerir:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eAlan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003etitle\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDüzeltme eyleminin kısa adı (örn. \u003ccode\u003e\u0026quot;Rotate AWS Access Key\u0026quot;\u003c/code\u003e)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esteps\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSırrı döndürmek veya iptal etmek için sıralı adımlar listesi\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edoc_url\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSağlayıcının resmi kimlik bilgisi yönetimi belgelerine bağlantı\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003econsole_url\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSağlayıcının yönetim konsolu sayfasına doğrudan bağlantı\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eurgency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNe kadar hızlı harekete geçileceği: \u003ccode\u003e\u0026quot;immediate\u0026quot;\u003c/code\u003e, \u003ccode\u003e\u0026quot;high\u0026quot;\u003c/code\u003e veya \u003ccode\u003e\u0026quot;medium\u0026quot;\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003echecklist\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDöndürme sonrası doğrulama adımları (örn. denetim günlüklerini inceleyin, güvenlik ekibini bilgilendirin)\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eLeakwatch, her yerleşik dedektör için bir tane olmak üzere 64 düzeltme girişiyle birlikte gelir. 64 girişin tamamı ikili dosyaya dahildir; rehberliği almak için herhangi bir ağ çağrısı yapılmaz.\u003c/p\u003e\n\u003ch2 id=\"her-formatta-nasl-grnr\"\u003eHer formatta nasıl görünür\u003c/h2\u003e\n\u003cp\u003eZenginleştirme, rehberliği bellekteki bulgu nesnesine ekler. Nasıl göründüğü çıktı formatına bağlıdır:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eJSON\u003c/strong\u003e — tam yapılandırılmış \u003ccode\u003eremediation\u003c/code\u003e nesnesi her bulgunun içine yerleştirilir:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --remediation --format json\n\u003c/code\u003e\u003c/pre\u003e\n\u003cpre\u003e\u003ccode class=\"language-json\"\u003e{\n \u0026quot;id\u0026quot;: \u0026quot;447b5d2846d08ce25dd3d638cfe911ad\u0026quot;,\n \u0026quot;detector_id\u0026quot;: \u0026quot;github-token\u0026quot;,\n \u0026quot;severity\u0026quot;: \u0026quot;critical\u0026quot;,\n \u0026quot;redacted\u0026quot;: \u0026quot;ghp_****************************Xk9R\u0026quot;,\n \u0026quot;source\u0026quot;: {\n \u0026quot;source_type\u0026quot;: \u0026quot;filesystem\u0026quot;,\n \u0026quot;file_path\u0026quot;: \u0026quot;scripts/deploy.sh\u0026quot;,\n \u0026quot;line\u0026quot;: 14\n },\n \u0026quot;verification\u0026quot;: {\n \u0026quot;status\u0026quot;: \u0026quot;verified_active\u0026quot;\n },\n \u0026quot;remediation\u0026quot;: {\n \u0026quot;title\u0026quot;: \u0026quot;Revoke GitHub Token\u0026quot;,\n \u0026quot;steps\u0026quot;: [\n \u0026quot;Go to GitHub Settings \u0026gt; Developer settings \u0026gt; Personal access tokens.\u0026quot;,\n \u0026quot;Revoke the compromised token immediately.\u0026quot;,\n \u0026quot;Create a new token with the minimum required scopes.\u0026quot;,\n \u0026quot;Update all integrations and CI/CD pipelines with the new token.\u0026quot;\n ],\n \u0026quot;doc_url\u0026quot;: \u0026quot;https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens\u0026quot;,\n \u0026quot;console_url\u0026quot;: \u0026quot;https://github.com/settings/tokens\u0026quot;,\n \u0026quot;urgency\u0026quot;: \u0026quot;immediate\u0026quot;,\n \u0026quot;checklist\u0026quot;: [\n \u0026quot;Review the GitHub audit log for unauthorized actions performed with the token.\u0026quot;,\n \u0026quot;Check repository and organization settings for unexpected changes.\u0026quot;,\n \u0026quot;Notify the security team about the exposure.\u0026quot;,\n \u0026quot;Scan for other repositories that may contain the same token.\u0026quot;\n ]\n },\n \u0026quot;entropy\u0026quot;: 5.82,\n \u0026quot;detected_at\u0026quot;: \u0026quot;2026-05-23T10:15:30Z\u0026quot;\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003cstrong\u003eSARIF\u003c/strong\u003e — \u003ccode\u003esteps\u003c/code\u003e alanları, kuralın \u003ccode\u003ehelp.text\u003c/code\u003e alanına yerleştirilir ve \u003ccode\u003edoc_url\u003c/code\u003e, kuralın \u003ccode\u003ehelpUri\u003c/code\u003e'si olarak ayarlanır. Bu, GitHub Code Scanning'in uyarı ayrıntıları panelinde doğrudan görünür.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eCSV\u003c/strong\u003e — yalnızca düzeltme \u003ccode\u003etitle\u003c/code\u003e'ı \u003ccode\u003eremediation\u003c/code\u003e sütununa yazılır. Tam yapılandırılmış rehberlik CSV çıktısına dahil edilmez.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTablo\u003c/strong\u003e — \u003ccode\u003eREMEDIATION\u003c/code\u003e sütununda yalnızca düzeltme \u003ccode\u003etitle\u003c/code\u003e'ı gösterilir.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --remediation --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eSEVERITY DETECTOR FILE LINE REDACTED STATUS REMEDIATION\n-------- -------- ---- ---- -------- ------ -----------\nCRITICAL github-token scripts/deploy.sh 14 ghp_****Xk9R verified_active Revoke GitHub Token\n\nFound 1 secret (1 critical).\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eİpucu\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eOtomatik olay müdahale iş akışları için tam yapılandırılmış rehberliğe ihtiyaç duyduğunuzda \u003ccode\u003e--remediation --format json\u003c/code\u003e kullanın. Terminalde hızlı, insan tarafından okunabilir bir önceliklendirme oturumu için \u003ccode\u003e--remediation --format table\u003c/code\u003e kullanın.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNot\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eZenginleştirme yalnızca \u003ccode\u003e--remediation\u003c/code\u003e ayarlandığında çalışır. Bayrak olmadan, \u003ccode\u003eremediation\u003c/code\u003e alanı JSON ve SARIF çıktısında yoktur, CSV \u003ccode\u003eremediation\u003c/code\u003e sütunu boştur ve tablo \u003ccode\u003eREMEDIATION\u003c/code\u003e sütunu \u003ccode\u003e-\u003c/code\u003e gösterir. Bayrak, orijinal tarama sonuçlarını değiştirmez — bunların üzerine bir katman ekler.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/output/output-formats\"\u003eÇıktı Formatları\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eDoğrulama Nasıl Çalışır\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n"},"reference/cli-reference":{"title":"CLI Başvurusu","description":"Her Leakwatch komutu, alt komutu ve bayrağı için tam başvuru kaynağı.","html":"\u003ch1 id=\"cli-bavurusu\"\u003eCLI Başvurusu\u003c/h1\u003e\n\u003cp\u003eBu sayfa, tüm Leakwatch komutları ve bayrakları için yetkili başvuru kaynağıdır. Kavramsal açıklamalar ve çalışma örnekleri için ilgili tarama veya yapılandırma sayfalarındaki çapraz bağlantıları takip edin.\u003c/p\u003e\n\u003ch2 id=\"global-bayraklar\"\u003eGlobal bayraklar\u003c/h2\u003e\n\u003cp\u003eBu bayraklar her komut ve alt komut üzerinde kullanılabilir.\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--config \u0026lt;path\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOtomatik olarak bulunan \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYapılandırma dosyasının yolu. Atlandığında Leakwatch, \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e için önce geçerli dizini, ardından kullanıcının ana dizinini arar — üst dizin taraması \u003cstrong\u003eyapılmaz\u003c/strong\u003e. Her iki konumdaki bozuk bir yapılandırma dosyası (geçersiz YAML) da ölümcül bir hatadır.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--log-level \u0026lt;level\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ewarn\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGünlük ayrıntı düzeyi: \u003ccode\u003edebug\u003c/code\u003e, \u003ccode\u003einfo\u003c/code\u003e, \u003ccode\u003ewarn\u003c/code\u003e veya \u003ccode\u003eerror\u003c/code\u003e. Günlük çıktısı stderr'e gider ve tarama sonuçlarını etkilemez.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"leakwatch-version\"\u003e\u003ccode\u003eleakwatch version\u003c/code\u003e\u003c/h2\u003e\n\u003cp\u003eİkili dosya sürümünü, commit karmasını ve derleme zaman damgasını yazdırır, ardından çıkar.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch version\n\u003c/code\u003e\u003c/pre\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eleakwatch v1.8.0 (commit: 1a2b3c4, built: 2026-08-11T12:00:00Z)\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"leakwatch-init\"\u003e\u003ccode\u003eleakwatch init\u003c/code\u003e\u003c/h2\u003e\n\u003cp\u003eGeçerli dizinde önerilen varsayılanlarla bir \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e yapılandırma dosyası oluşturur.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch init [flags]\n\u003c/code\u003e\u003c/pre\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output \u0026lt;path\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e.leakwatch.yaml\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYapılandırma dosyasını varsayılan yerine bu yola yaz.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--force\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMevcut bir yapılandırma dosyasının üzerine yaz. Bu bayrak olmadan, çıktı dosyası zaten mevcutsa \u003ccode\u003einit\u003c/code\u003e hatayla çıkar.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Varsayılan yapılandırmayı oluştur\nleakwatch init\n\n# Mevcut yapılandırmanın üzerine yaz\nleakwatch init --force\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"leakwatch-scan\"\u003e\u003ccode\u003eleakwatch scan\u003c/code\u003e\u003c/h2\u003e\n\u003cp\u003eTüm tarama alt komutları için üst komut. Kendi başına davranışı yoktur; bir alt komut çalıştırın.\u003c/p\u003e\n\u003ch3 id=\"ortak-tarama-bayraklar\"\u003eOrtak tarama bayrakları\u003c/h3\u003e\n\u003cp\u003eAşağıdaki bayraklar \u003cstrong\u003etüm\u003c/strong\u003e \u003ccode\u003escan\u003c/code\u003e alt komutlarında kullanılabilir.\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eKısa\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-f\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ejson\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktı biçimi: \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e veya \u003ccode\u003egithub\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-o\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estdout\u003c/td\u003e\n\u003ctd\u003eSonuçları stdout yerine bu dosya yoluna yaz. Uzantısız düz bir yol verildiğinde, biçimin kendi uzantısı otomatik olarak eklenir (örn. \u003ccode\u003e--format sarif --output results\u003c/code\u003e, \u003ccode\u003eresults.sarif\u003c/code\u003e dosyasını yazar). Dosya \u003ccode\u003e0600\u003c/code\u003e izinleriyle yazılır. Her zaman stdout'a yazan \u003ccode\u003e--format github\u003c/code\u003e için yok sayılır.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-c\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCPU sayısı\u003c/td\u003e\n\u003ctd\u003eEşzamanlı tarama çalışanı sayısı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--max-file-size\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e10485760\u003c/code\u003e (10 MB)\u003c/td\u003e\n\u003ctd\u003eBu bayt sayısından büyük dosyaları veya blob'ları atla.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--show-raw\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktıya ham (maskelenmemiş) sır değerini dahil et. Dikkatli kullanın.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-detectors\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eBu çalıştırma için hariç tutulacak dedektör kimlikleri (örn. \u003ccode\u003eaws-access-key-id,generic-api-key\u003c/code\u003e). Tekrarlanabilir veya virgülle ayrılabilir; yapılandırma dosyasındaki \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e ile birleştirilir (onun yerine geçmez).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--no-verify\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCanlı sır doğrulamasını devre dışı bırak. Giden API çağrısı yapılmaz.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYalnızca canlı doğrulama ile etkin olduğu teyit edilen bulguları raporla.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--verifier-origin \u0026lt;id=url\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eBağlam gerektiren bir dedektörü operatörce güvenilen HTTPS origin'ine bağla. Tekrarlanabilir; yalnız komut satırında kabul edilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--grafana-instance-url \u0026lt;url\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eGüvenilir Grafana origin'ini sağlayan geriye uyumlu alias.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktıya dahil edilecek minimum önem derecesi: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e veya \u003ccode\u003ecritical\u003c/code\u003e. Tanınmayan bir değer (örn. bir yazım hatası) sessizce kabul edilmek yerine kesin bir hataya yol açar.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--remediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHer bulguya düzeltme rehberi (dönüşüm/iptal adımları) ekle.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e (tekrarlanabilir) bayrağı, \u003ccode\u003escan slack\u003c/code\u003e \u003cstrong\u003ehariç\u003c/strong\u003e her tarama alt komutunda da kullanılabilir — bu bayrak tüm alt komutlar arasında paylaşılmak yerine komut başına kaydedildiğinden, aşağıda her alt komutun kendi bayrak tablosuna bakın.\u003c/p\u003e\n\u003cp\u003eÇıkış kodu sözleşmesinin tamamı için \u003ca href=\"#/reference/exit-codes\"\u003eÇıkış Kodları\u003c/a\u003e sayfasına bakın: \u003ccode\u003e0\u003c/code\u003e temiz, \u003ccode\u003e1\u003c/code\u003e bulgu var, \u003ccode\u003e2\u003c/code\u003e hata, \u003ccode\u003e3\u003c/code\u003e tamamlanmadan kesildi.\u003c/p\u003e\n\u003chr\u003e\n\u003ch3 id=\"scan-fs\"\u003e\u003ccode\u003escan fs\u003c/code\u003e\u003c/h3\u003e\n\u003cp\u003eBir veya daha fazla yerel dosya sistemi yolunu tarar — her biri bir dizin (özyinelemeli olarak dolaşılır) veya tek bir dosya olabilir.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs [path...] [flags]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003cstrong\u003eSıfır veya daha fazla\u003c/strong\u003e konumsal argüman kabul eder. Yol verilmediğinde \u003ccode\u003e.\u003c/code\u003e (geçerli dizin) taranır. Tek bir çağrıda dosya ve dizinler karıştırılarak birden fazla yol verilebilir.\u003c/p\u003e\n\u003ch4 id=\"dosya-sistemine-zg-bayraklar\"\u003eDosya sistemine özgü bayraklar\u003c/h4\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eDışlanacak yollar için glob kalıbı. Tekrarlanabilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch4 id=\"rnekler\"\u003eÖrnekler\u003c/h4\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Geçerli dizini tara, renklendirilmiş tablo yazdır\nleakwatch scan fs . --format table\n\n# Tek bir dosyayı tara\nleakwatch scan fs cmd/main.go\n\n# Birden fazla dosya ve dizini birlikte tara\nleakwatch scan fs cmd/ main.go internal/config\n\n# SARIF çıktısını kaydet, test dosyalarını ve vendor'ı dışla\nleakwatch scan fs . \\\n --exclude \u0026quot;**/*_test.go\u0026quot; \\\n --exclude \u0026quot;vendor/**\u0026quot; \\\n --format sarif \\\n --output results.sarif\n\n# Bu çalıştırma için belirli dedektörleri hariç tut\nleakwatch scan fs . --exclude-detectors aws-access-key-id,generic-api-key\n\u003c/code\u003e\u003c/pre\u003e\n\u003chr\u003e\n\u003ch3 id=\"scan-git\"\u003e\u003ccode\u003escan git\u003c/code\u003e\u003c/h3\u003e\n\u003cp\u003eYerel veya uzak bir Git deposunun tam commit geçmişini tarar.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git \u0026lt;url_or_path\u0026gt; [flags]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eTam olarak bir konumsal argüman gereklidir: yerel bir yol veya HTTP/HTTPS/SSH URL'si.\u003c/p\u003e\n\u003ch4 id=\"gite-zg-bayraklar\"\u003eGit'e özgü bayraklar\u003c/h4\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--since \u0026lt;YYYY-MM-DD\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eYalnızca bu tarihten sonraki commit'leri tara.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--since-commit \u0026lt;hash\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eYalnızca bu commit karmasından HEAD'e kadar olan değişiklikleri tara.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--branch \u0026lt;name\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eVarsayılan dal yerine belirli bir dalı hedefle.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--depth \u0026lt;int\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e (tam)\u003c/td\u003e\n\u003ctd\u003eUzak depolar için sığ klonlama derinliği. \u003ccode\u003e0\u003c/code\u003e tam geçmişi getirir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eDışlanacak blob yolları için glob kalıbı. Tekrarlanabilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch4 id=\"rnekler-1\"\u003eÖrnekler\u003c/h4\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Tam yerel geçmişi tara\nleakwatch scan git . --format table\n\n# Bir pull request tarafından eklenen commit'leri tara\nleakwatch scan git . --since-commit a1b2c3d --format json\n\u003c/code\u003e\u003c/pre\u003e\n\u003chr\u003e\n\u003ch3 id=\"scan-image\"\u003e\u003ccode\u003escan image\u003c/code\u003e\u003c/h3\u003e\n\u003cp\u003eBir OCI/Docker imajının katmanlarını sırlar açısından tarar. Leakwatch daemonsuz çalışır ve kayıt defterinden doğrudan çeker — Docker soketi gerekmez.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan image \u0026lt;image:tag\u0026gt; [flags]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eTam olarak bir konumsal argüman gereklidir. Ortak tarama bayraklarının ötesinde imaja özgü bir bayrak yoktur; ortak bayraklar arasında, katmanlar içindeki dosya yollarıyla eşleştirilen \u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e (tekrarlanabilir) de bulunur.\u003c/p\u003e\n\u003ch4 id=\"rnekler-2\"\u003eÖrnekler\u003c/h4\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Genel bir imajı tara\nleakwatch scan image nginx:latest --format table\n\n# Özel kayıt defteri imajını tara, JSON çıktısını kaydet\nleakwatch scan image registry.example.com/my-app:v2.3.0 \\\n --format json \\\n --output image-results.json\n\u003c/code\u003e\u003c/pre\u003e\n\u003chr\u003e\n\u003ch3 id=\"scan-s3\"\u003e\u003ccode\u003escan s3\u003c/code\u003e\u003c/h3\u003e\n\u003cp\u003eBir AWS S3 kovasındaki nesneleri tarar.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan s3 \u0026lt;bucket\u0026gt; [flags]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eTam olarak bir konumsal argüman gereklidir.\u003c/p\u003e\n\u003ch4 id=\"s3e-zg-bayraklar\"\u003eS3'e özgü bayraklar\u003c/h4\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--prefix \u0026lt;string\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eTaramayı, anahtarı bu ön ekle başlayan nesnelerle sınırla.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--region \u0026lt;string\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eKovanın bulunduğu AWS bölgesi. \u003ccode\u003eAWS_REGION\u003c/code\u003e ortam değişkenine veya AWS SDK varsayılanına geri döner.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eDışlanacak nesne anahtarları için glob kalıbı. Tekrarlanabilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch4 id=\"rnekler-3\"\u003eÖrnekler\u003c/h4\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Tüm kovayı tara\nleakwatch scan s3 my-data-bucket --region us-east-1 --format table\n\n# Belirli bir ön eki tara\nleakwatch scan s3 my-data-bucket --prefix backups/2026/ --format json\n\u003c/code\u003e\u003c/pre\u003e\n\u003chr\u003e\n\u003ch3 id=\"scan-gcs\"\u003e\u003ccode\u003escan gcs\u003c/code\u003e\u003c/h3\u003e\n\u003cp\u003eBir Google Cloud Storage kovasındaki nesneleri tarar.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan gcs \u0026lt;bucket\u0026gt; [flags]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eTam olarak bir konumsal argüman gereklidir.\u003c/p\u003e\n\u003ch4 id=\"gcsye-zg-bayraklar\"\u003eGCS'ye özgü bayraklar\u003c/h4\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--prefix \u0026lt;string\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eTaramayı, adı bu ön ekle başlayan nesnelerle sınırla.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--project \u0026lt;string\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eGCP proje kimliği. Varsayılan kimlik bilgilerinden proje çıkarılamadığında gereklidir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eDışlanacak nesne adları için glob kalıbı. Tekrarlanabilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch4 id=\"rnekler-4\"\u003eÖrnekler\u003c/h4\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Tüm GCS kovasını tara\nleakwatch scan gcs my-gcs-bucket --project my-gcp-project --format table\n\n# Ön ek tara\nleakwatch scan gcs my-gcs-bucket --prefix uploads/2026/ --format json\n\u003c/code\u003e\u003c/pre\u003e\n\u003chr\u003e\n\u003ch3 id=\"scan-slack\"\u003e\u003ccode\u003escan slack\u003c/code\u003e\u003c/h3\u003e\n\u003cp\u003eBir Slack çalışma alanındaki mesaj metnini tarar.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan slack [flags]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eKonumsal argüman yoktur.\u003c/p\u003e\n\u003ch4 id=\"slacke-zg-bayraklar\"\u003eSlack'e özgü bayraklar\u003c/h4\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--token \u0026lt;string\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eSlack bot token'ı. \u003ccode\u003eLEAKWATCH_SLACK_TOKEN\u003c/code\u003e ortam değişkeni ile de ayarlanabilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--channels \u0026lt;list\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eTaranacak kanal adları veya kimliklerinin virgülle ayrılmış listesi. Atlandığında erişilebilir tüm kanalları tarar.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-channels \u0026lt;list\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eAtlanacak kanal adları veya kimliklerinin virgülle ayrılmış listesi.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--since \u0026lt;YYYY-MM-DD\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eYalnızca bu tarihten sonra gönderilen mesajları tara.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--include-dms\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDoğrudan mesajları dahil et (ek OAuth kapsamları gerektirir).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--include-files\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBoyutu sınırlı metin benzeri ekleri indir ve tara. \u003ccode\u003efiles:read\u003c/code\u003e gerektirir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--rate-limit \u0026lt;float\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSaniyelik isteğe bağlı işlem-başı istek tavanı. Sıfır, Slack için güvenli işlem-özel varsayılanları korur.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003ccode\u003escan slack\u003c/code\u003e komutunda (diğer her tarama alt komutunun aksine) \u003ccode\u003e--exclude\u003c/code\u003e yol-kalıbı bayrağı yoktur — bunun yerine tüm kanalları atlamak için \u003ccode\u003e--exclude-channels\u003c/code\u003e kullanın.\u003c/p\u003e\n\u003ch4 id=\"rnekler-5\"\u003eÖrnekler\u003c/h4\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Erişilebilir tüm kanalları tara\nleakwatch scan slack --token xoxb-••••••••••••-••••••••••••-•••••••••••••••••••••••• --format table\n\n# Belirli kanalları belirli bir tarihten itibaren tara\nleakwatch scan slack \\\n --token xoxb-••••••••••••-••••••••••••-••••••••••••••••••••••••• \\\n --channels general,engineering \\\n --since 2026-01-01 \\\n --format json\n\u003c/code\u003e\u003c/pre\u003e\n\u003chr\u003e\n\u003ch3 id=\"scan-repos\"\u003e\u003ccode\u003escan repos\u003c/code\u003e\u003c/h3\u003e\n\u003cp\u003eBirden fazla Git deposunu paralel olarak tarar.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan repos \u0026lt;url_or_path...\u0026gt; [flags]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eEn az iki konumsal argüman (depo URL'leri veya yerel yollar) gereklidir.\u003c/p\u003e\n\u003ch4 id=\"reposa-zg-bayraklar\"\u003eRepos'a özgü bayraklar\u003c/h4\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eKısa\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--parallel\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eEşzamanlı olarak taranacak depo sayısı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-c\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCPU sayısı\u003c/td\u003e\n\u003ctd\u003eHer depo taramasındaki çalışan eşzamanlılığı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude \u0026lt;pattern\u0026gt;\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eHer depoya uygulanan, dışlanacak blob yolları için glob kalıbı. Tekrarlanabilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch4 id=\"rnekler-6\"\u003eÖrnekler\u003c/h4\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# İki depoyu paralel olarak tara\nleakwatch scan repos \\\n https://github.com/org/repo-a.git \\\n https://github.com/org/repo-b.git \\\n --format json\n\n# Büyük bir depo seti için paralellizmi artır\nleakwatch scan repos \\\n https://github.com/org/repo-a.git \\\n https://github.com/org/repo-b.git \\\n https://github.com/org/repo-c.git \\\n --parallel 3 \\\n --format sarif \\\n --output multi-repo.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003chr\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/reference/exit-codes\"\u003eÇıkış Kodları\u003c/a\u003e — çıkış kodlarının tarama sonuçlarıyla nasıl eşleştiği.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/environment-variables\"\u003eOrtam Değişkenleri\u003c/a\u003e — Leakwatch'ı bayrak kullanmadan yapılandırma.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/filesystem\"\u003eDosya Sistemi Taraması\u003c/a\u003e — ayrıntılı \u003ccode\u003escan fs\u003c/code\u003e rehberi.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/git-history\"\u003eGit Geçmişi\u003c/a\u003e — ayrıntılı \u003ccode\u003escan git\u003c/code\u003e rehberi.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e — \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e başvurusu.\u003c/li\u003e\n\u003c/ul\u003e\n"},"reference/environment-variables":{"title":"Ortam Değişkenleri","description":"Leakwatch davranışını bayrak kullanmadan yapılandıran ortam değişkenleri.","html":"\u003ch1 id=\"ortam-deikenleri\"\u003eOrtam Değişkenleri\u003c/h1\u003e\n\u003cp\u003eLeakwatch, yapılandırmayı öncelik sırasına göre üç kaynaktan okur: \u003cstrong\u003ekomut satırı bayrakları\u003c/strong\u003e, \u003cstrong\u003eortam değişkenlerini\u003c/strong\u003e geçersiz kılar; ortam değişkenleri \u003cstrong\u003eyapılandırma dosyasını\u003c/strong\u003e (\u003ccode\u003e.leakwatch.yaml\u003c/code\u003e) geçersiz kılar; yapılandırma dosyası yerleşik \u003cstrong\u003evarsayılanlara\u003c/strong\u003e geri döner. Ortam değişkenleri, bir yapılandırma dosyasını değiştiremeyeceğiniz veya her çağrıya bayrak geçiremeyeceğiniz CI ortamlarında kullanışlıdır.\u003c/p\u003e\n\u003ch2 id=\"yaplandrma-deikeni-kalb\"\u003eYapılandırma değişkeni kalıbı\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003e.leakwatch.yaml\u003c/code\u003e'daki herhangi bir anahtar, ortam değişkeni olarak şu şekilde ayarlanabilir:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eAnahtar adını büyük harfe çevir.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e.\u003c/code\u003e ve \u003ccode\u003e-\u003c/code\u003e karakterlerini \u003ccode\u003e_\u003c/code\u003e ile değiştir.\u003c/li\u003e\n\u003cli\u003eBaşına \u003ccode\u003eLEAKWATCH_\u003c/code\u003e ekle.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eÖrneğin, \u003ccode\u003escan.concurrency\u003c/code\u003e yapılandırma anahtarı \u003ccode\u003eLEAKWATCH_SCAN_CONCURRENCY\u003c/code\u003e olur.\u003c/p\u003e\n\u003ch2 id=\"deiken-bavurusu\"\u003eDeğişken başvurusu\u003c/h2\u003e\n\u003ch3 id=\"leakwatcha-zg-deikenler\"\u003eLeakwatch'a özgü değişkenler\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eDeğişken\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_SLACK_TOKEN\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003escan slack\u003c/code\u003e için Slack bot token'ı. \u003ccode\u003e--token\u003c/code\u003e'a eşdeğer. Token'ın kabuk geçmişinde veya CI günlüklerinde görünmesini önlemek için bayrak olarak geçirmek yerine bunu ayarlayın.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_SCAN_CONCURRENCY\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eEşzamanlı tarama çalışanı sayısı. \u003ccode\u003e--concurrency\u003c/code\u003e'e eşdeğer.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_VERIFICATION_ENABLED\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCanlı doğrulamayı genel olarak devre dışı bırakmak için \u003ccode\u003efalse\u003c/code\u003e olarak ayarlayın. \u003ccode\u003e--no-verify\u003c/code\u003e'e eşdeğer.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_VERIFICATION_RATE_LIMIT\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTüm doğrulayıcılar genelinde saniye başına maksimum doğrulama isteği.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_OUTPUT_FORMAT\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eVarsayılan çıktı biçimi (\u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e veya \u003ccode\u003egithub\u003c/code\u003e). \u003ccode\u003e--format\u003c/code\u003e'a eşdeğer.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eLEAKWATCH_DETECTION_ENTROPY_THRESHOLD\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBir eşleşmenin raporlanması için gereken minimum Shannon entropisi. Float değer, örn. \u003ccode\u003e3.5\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"grntleme-deikeni\"\u003eGörüntüleme değişkeni\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eDeğişken\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eNO_COLOR\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBoş olmayan herhangi bir değere ayarlandığında, \u003ccode\u003etable\u003c/code\u003e çıktı biçimlendiricisindeki ANSI renk kodlarını devre dışı bırakır. \u003ca href=\"https://no-color.org\"\u003eno-color.org\u003c/a\u003e kuralını izler.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"aws-deikenleri-scan-s3-ve-aws-sr-dorulamas-iin\"\u003eAWS değişkenleri (\u003ccode\u003escan s3\u003c/code\u003e ve AWS sır doğrulaması için)\u003c/h3\u003e\n\u003cp\u003eBunlar standart AWS SDK ortam değişkenleridir. Leakwatch bunları AWS SDK v2 varsayılan kimlik bilgisi zincirine aktarır.\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eDeğişken\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eAWS_ACCESS_KEY_ID\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAWS erişim anahtarı kimliği.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eAWS_SECRET_ACCESS_KEY\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAWS gizli erişim anahtarı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eAWS_SESSION_TOKEN\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAWS oturum token'ı (geçici kimlik bilgileri için).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eAWS_REGION\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eVarsayılan AWS bölgesi.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eAWS_PROFILE\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eKullanılacak \u003ccode\u003e~/.aws/credentials\u003c/code\u003e dosyasından adlandırılmış profil.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"gcs-deikeni-scan-gcs-iin\"\u003eGCS değişkeni (\u003ccode\u003escan gcs\u003c/code\u003e için)\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eDeğişken\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eGOOGLE_APPLICATION_CREDENTIALS\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGoogle hizmet hesabı JSON anahtar dosyasının yolu. Bir GCS kovasını tararken Uygulama Varsayılan Kimlik Bilgileri tarafından kullanılır.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"ncelik-rnei\"\u003eÖncelik örneği\u003c/h2\u003e\n\u003cp\u003eŞu kurulumu varsayın:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e.leakwatch.yaml\u003c/code\u003e, \u003ccode\u003eoutput.format: table\u003c/code\u003e olarak ayarlıyor\u003c/li\u003e\n\u003cli\u003eOrtamda \u003ccode\u003eLEAKWATCH_OUTPUT_FORMAT=json\u003c/code\u003e ayarlanmış\u003c/li\u003e\n\u003cli\u003eKomut \u003ccode\u003eleakwatch scan fs .\u003c/code\u003e olarak çalıştırılıyor (\u003ccode\u003e--format\u003c/code\u003e bayrağı yok)\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eOrtam değişkeni yapılandırma dosyasını geçersiz kıldığından geçerli biçim \u003ccode\u003ejson\u003c/code\u003e'dır.\u003c/p\u003e\n\u003cp\u003eKomut \u003ccode\u003eleakwatch scan fs . --format sarif\u003c/code\u003e olarak çalıştırılırsa, bayrak her şeyi geçersiz kıldığından geçerli biçim \u003ccode\u003esarif\u003c/code\u003e olur.\u003c/p\u003e\n\u003ch2 id=\"dorulama-kimlik-bilgileri-ve-tarama-kimlik-bilgileri\"\u003eDoğrulama kimlik bilgileri ve tarama kimlik bilgileri\u003c/h2\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNot\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eYukarıdaki AWS ve GCP değişkenleri, Leakwatch'ın \u003cstrong\u003ekendisinin\u003c/strong\u003e nesneleri taramak için S3 veya GCS'ye bağlanırken kimliğini doğrulaması için kullanılır. Bulunan sırları doğrulamak için kullanılmazlar. Keşfedilen bir AWS anahtarının doğrulanması, örneğin, runner'ın kimlik bilgilerini değil, keşfedilen anahtarın kendisini kullanarak AWS STS'yi çağırır.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"cida-srlar-gvenli-biimde-geirme\"\u003eCI'da sırları güvenli biçimde geçirme\u003c/h2\u003e\n\u003cp\u003eGitHub Actions'ta şifrelenmiş sırları kullanın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003eenv:\n LEAKWATCH_SLACK_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eGitLab CI'da maskelenmiş CI/CD değişkenlerini kullanın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003evariables:\n LEAKWATCH_SLACK_TOKEN: $SLACK_BOT_TOKEN # proje ayarlarında maskelenmiş değişken olarak tanımlanmış\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eToken değerlerini hiçbir zaman iş akışı dosyalarına veya Dockerfile'lara sabit olarak kodlamayın.\u003c/p\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e — tam \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e anahtar başvurusu.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/cloud-storage\"\u003eBulut Depolama Taraması\u003c/a\u003e — \u003ccode\u003escan s3\u003c/code\u003e ve \u003ccode\u003escan gcs\u003c/code\u003e kimlik bilgileri.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/slack\"\u003eSlack Taraması\u003c/a\u003e — Slack token kapsamları ve izinleri.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Başvurusu\u003c/a\u003e — eşdeğer komut satırı bayrakları.\u003c/li\u003e\n\u003c/ul\u003e\n"},"reference/exit-codes":{"title":"Çıkış Kodları","description":"Leakwatch çıkış kodu başvurusu ve bunların betiklerde ve CI pipeline'larında nasıl kullanılacağı.","html":"\u003ch1 id=\"k-kodlar\"\u003eÇıkış Kodları\u003c/h1\u003e\n\u003cp\u003eLeakwatch, CI pipeline'larının ve kabuk betiklerinin çıktıyı ayrıştırmadan tarama sonuçlarına göre hareket edebilmesi için küçük, iyi tanımlanmış bir çıkış kodu seti kullanır. Her tarama alt komutu dört koddan biriyle çıkar.\u003c/p\u003e\n\u003ch2 id=\"kod-bavurusu\"\u003eKod başvurusu\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eKod\u003c/th\u003e\n\u003cth\u003eAd\u003c/th\u003e\n\u003cth\u003eAnlam\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTemiz\u003c/td\u003e\n\u003ctd\u003eTarama başarıyla tamamlandı ve etkin filtrelerden hiçbir bulgu geçmedi.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBulgular var\u003c/td\u003e\n\u003ctd\u003eTarama tamamlandı ve etkin filtrelerden geçen bir veya daha fazla sır bulundu (ve etkin filtrelerden geçti). Bu, diğer her sonuca göre önceliklidir: bulgular raporlanmışsa, tarama daha sonra kesintiye uğrasa veya çoklu depo taramasındaki bir depo başarısız olsa bile çıkış kodu \u003ccode\u003e1\u003c/code\u003e döndürülür.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHata\u003c/td\u003e\n\u003ctd\u003eTaramanın hiç çalışamamasına neden olan ciddi bir hata oluştu — örneğin geçersiz bir bayrak, okunamaz bir yol veya kimlik doğrulama hatası. Stderr'e bir \u003ccode\u003eError: ...\u003c/code\u003e mesajı ve kullanım ipucu yazdırılır.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eKesintiye uğradı\u003c/td\u003e\n\u003ctd\u003eTarama tamamlanmadan önce \u003ccode\u003eSIGINT\u003c/code\u003e (\u003ccode\u003eCtrl+C\u003c/code\u003e) veya \u003ccode\u003eSIGTERM\u003c/code\u003e aldı ve kesinti anında \u003cstrong\u003ehiçbir\u003c/strong\u003e bulgu raporlamamıştı. Kısmi sonuçlar (filtrelemeden geçen varsa) yine de yapılandırılmış çıktıya yazılır. Bu nedenle bir CI hattı, tamamlanmadan biten bir tarama için asla temiz bir \u003ccode\u003e0\u003c/code\u003e çıkışı gözlemleyemez.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"filtrelerin-k-kodu-1i-nasl-etkiledii\"\u003eFiltrelerin çıkış kodu 1'i nasıl etkilediği\u003c/h2\u003e\n\u003cp\u003eÇıkış kodu \u003ccode\u003e1\u003c/code\u003e, yalnızca en az bir bulgu etkin çıktı filtrelerinin tümünden geçtiğinde yayılır. En ilgili iki filtre şunlardır:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/strong\u003e — eşiğin altındaki bulgular bastırılır. Tüm bulgular \u003ccode\u003elow\u003c/code\u003e önem derecesindeyse ve \u003ccode\u003e--min-severity high\u003c/code\u003e ile çalışıyorsanız, sırlar mevcut olmasına rağmen çıkış kodu \u003ccode\u003e0\u003c/code\u003e döndürülür.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/strong\u003e — yalnızca canlı doğrulama ile etkin olduğu teyit edilen bulgular raporlanır. Etkin sır bulunamazsa çıkış kodu \u003ccode\u003e0\u003c/code\u003e döndürülür.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eBu, çıkış kodu \u003ccode\u003e0\u003c/code\u003e'ın \u0026quot;mevcut filtre ayarlarınızla eşleşen bulgu yok\u0026quot; anlamına geldiği anlamına gelir — kod tabanının hiçbir sır içermediği değil.\u003c/p\u003e\n\u003cdiv class=\"callout callout-warn\"\u003e\u003cdiv class=\"callout-label\"\u003eUyarı\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003e\u003ccode\u003e--only-verified\u003c/code\u003e altında temiz \u003ccode\u003e0\u003c/code\u003e çıkışı, kod tabanının sırdan arındırılmış olduğunu garanti etmez. Doğrulamanın mevcut olmadığı sır türleri (11 dedektör türü) her zaman doğrulanmamış olarak raporlanır ve \u003ccode\u003e--only-verified\u003c/code\u003e tarafından bastırılır. Tam kapsam için \u003ccode\u003e--only-verified\u003c/code\u003e ile birlikte ayrı bir filtresiz tarama yapın.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"kabuk-betiklerinde-k-kodlarn-kullanma\"\u003eKabuk betiklerinde çıkış kodlarını kullanma\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e#!/usr/bin/env bash\nset +e\nleakwatch scan fs . --format json --output leakwatch.json --no-verify\nEXIT_CODE=$?\nset -e\n\ncase \u0026quot;$EXIT_CODE\u0026quot; in\n 0)\n echo \u0026quot;Sır bulunamadı. Derleme devam ediyor.\u0026quot;\n ;;\n 1)\n echo \u0026quot;Sırlar bulundu — birleştirmeden önce leakwatch.json'u inceleyin ve düzeltin.\u0026quot;\n exit 1\n ;;\n 3)\n echo \u0026quot;Tarama tamamlanmadan kesintiye uğradı — güvenilmez sayın, birleştirmeyin.\u0026quot;\n exit 3\n ;;\n *)\n echo \u0026quot;Leakwatch bir hatayla karşılaştı (çıkış $EXIT_CODE).\u0026quot;\n exit \u0026quot;$EXIT_CODE\u0026quot;\n ;;\nesac\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eTaramadan önce \u003ccode\u003eset +e\u003c/code\u003e kullanmak, kabuğun sıfır dışı kodlarda çıkmasını engeller ve kodu kendiniz yakalayıp işlemenize olanak tanır.\u003c/p\u003e\n\u003ch2 id=\"ci-pipelinelarnda-k-kodlarn-kullanma\"\u003eCI pipeline'larında çıkış kodlarını kullanma\u003c/h2\u003e\n\u003cp\u003eÇoğu CI sistemi, sıfır dışı herhangi bir çıkış kodunu adım başarısızlığı olarak değerlendirir. Leakwatch sırlar bulunduğunda \u003ccode\u003e1\u003c/code\u003e ile çıktığından, ek yapılandırma olmadan pipeline otomatik olarak başarısız olur — yalnızca tarama komutunu çalıştırın.\u003c/p\u003e\n\u003cp\u003eSırlar bulunsa bile pipeline'ın devam etmesine izin vermek için (örneğin, derlemeyi engellemeden raporu toplamak amacıyla) çıkış kodunu açıkça yoksayın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --format sarif --output results.sarif --no-verify || true\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eYa da GitLab CI'da:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003eallow_failure: true\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eYa da GitHub Action'da \u003ccode\u003efail-on-findings: \u0026quot;false\u0026quot;\u003c/code\u003e olarak ayarlayın.\u003c/p\u003e\n\u003ch2 id=\"uygulamada-k-kodu-2\"\u003eUygulamada çıkış kodu 2\u003c/h2\u003e\n\u003cp\u003eÇıkış kodu \u003ccode\u003e2\u003c/code\u003e, taramanın hiç çalışamamasına neden olan bir yapılandırma veya çalışma zamanı hatasını gösterir. Yaygın nedenler:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eGeçersiz bir bayrak değeri (örneğin \u003ccode\u003e--format invalid\u003c/code\u003e).\u003c/li\u003e\n\u003cli\u003eMevcut olmayan veya okunamaz bir yol.\u003c/li\u003e\n\u003cli\u003eEksik gerekli argüman (örneğin, URL olmadan \u003ccode\u003escan git\u003c/code\u003e).\u003c/li\u003e\n\u003cli\u003eBir bulut kaynağına bağlanırken kimlik doğrulama hatası.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eHata mesajı stderr'e yazdırılır ve sorunu teşhis etmeye yardımcı olacak bağlam içerir. Örneğin, geçersiz bir \u003ccode\u003e--format\u003c/code\u003e değeri şunu üretir:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-text\"\u003eError: failed to load configuration: unsupported output format: xlsx\n\nRun 'leakwatch scan fs --help' for usage information.\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eGeçerli \u003ccode\u003e--format\u003c/code\u003e değerleri \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e ve \u003ccode\u003egithub\u003c/code\u003e'dır.\u003c/p\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/other-ci\"\u003eDiğer CI Sistemleri\u003c/a\u003e — çıkış kodlarını GitLab CI, Jenkins ve diğerlerine bağlama.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/ci-cd/github-action\"\u003eGitHub Action\u003c/a\u003e — resmi action'ın çıkış kodlarını adım sonuçlarıyla nasıl eşlediği.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Başvurusu\u003c/a\u003e — tam bayrak başvurusu.\u003c/li\u003e\n\u003c/ul\u003e\n"},"scanning/cloud-storage":{"title":"Bulut Depolama (S3 \u0026 GCS)","description":"AWS S3 ve Google Cloud Storage kovalarını sızan sırlara karşı tarayın.","html":"\u003ch1 id=\"bulut-depolama-s3--gcs\"\u003eBulut Depolama (S3 \u0026amp; GCS)\u003c/h1\u003e\n\u003cp\u003eSırlar sıklıkla bulut depolamaya taşınır — dışa aktarılan veritabanı dökümleri, ortam dosyaları, CI artefaktları ve günlük arşivleri, düşünüldüğünden çok daha fazla kişinin erişebildiği kovalara akar. Leakwatch, AWS S3 ve Google Cloud Storage kovalarını nesne nesne tarayabilir ve bulduğu sırları bir olaya dönüşmeden işaretler.\u003c/p\u003e\n\u003ch2 id=\"aws-s3\"\u003eAWS S3\u003c/h2\u003e\n\u003ch3 id=\"kullanm\"\u003eKullanım\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan s3 \u0026lt;bucket\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eKomut tam olarak bir argüman alır: \u003cstrong\u003ekova adı\u003c/strong\u003e (\u003ccode\u003es3://\u003c/code\u003e öneki olmadan). Tarama hedefi \u003ccode\u003es3://\u0026lt;bucket\u0026gt;\u003c/code\u003e olarak gösterilir.\u003c/p\u003e\n\u003ch3 id=\"kimlik-dorulama\"\u003eKimlik doğrulama\u003c/h3\u003e\n\u003cp\u003eLeakwatch standart \u003ca href=\"https://docs.aws.amazon.com/sdkref/latest/guide/standardized-credentials.html\"\u003eAWS varsayılan kimlik bilgisi zincirini\u003c/a\u003e kullanır:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eOrtam değişkenleri (\u003ccode\u003eAWS_ACCESS_KEY_ID\u003c/code\u003e, \u003ccode\u003eAWS_SECRET_ACCESS_KEY\u003c/code\u003e, \u003ccode\u003eAWS_SESSION_TOKEN\u003c/code\u003e).\u003c/li\u003e\n\u003cli\u003ePaylaşılan kimlik bilgileri dosyası (\u003ccode\u003e~/.aws/credentials\u003c/code\u003e).\u003c/li\u003e\n\u003cli\u003ePaylaşılan yapılandırma dosyası (\u003ccode\u003e~/.aws/config\u003c/code\u003e).\u003c/li\u003e\n\u003cli\u003eÖrneğe veya göreve atanmış IAM rolü (EC2, ECS, Lambda).\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eAWS CLI ile zaten kimlik doğrulaması yaptıysanız (\u003ccode\u003eaws configure\u003c/code\u003e veya üstlenilmiş bir rol) ek yapılandırma gerekmez.\u003c/p\u003e\n\u003ch3 id=\"s3e-zg-bayraklar\"\u003eS3'e özgü bayraklar\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eTür\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--prefix\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eYalnızca anahtarı bu önekle başlayan nesneleri tara.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--region\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003eAWS yapılandırmasından\u003c/td\u003e\n\u003ctd\u003eKovanın bulunduğu AWS bölgesi.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"s3-rnekleri\"\u003eS3 örnekleri\u003c/h3\u003e\n\u003cp\u003eTüm kovayı tarayın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan s3 my-config-bucket\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBelirli bir bölgede belirli bir anahtar öneki altındaki nesneleri tarayın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan s3 my-bucket --prefix logs/ --region us-east-1\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eSARIF olarak kaydedin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan s3 my-bucket --format sarif --output s3-results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eİpucu\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eTaramayı ilgili bir alt yola sınırlamak için \u003ccode\u003e--prefix\u003c/code\u003e kullanın. Milyonlarca nesne içeren büyük bir kovayı taramak yavaş olabilir ve S3 GET istek maliyeti doğurabilir. Öneki gerçekten önemli olana — örneğin \u003ccode\u003econfigs/\u003c/code\u003e veya \u003ccode\u003eexports/\u003c/code\u003e — daraltın.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003chr\u003e\n\u003ch2 id=\"google-cloud-storage\"\u003eGoogle Cloud Storage\u003c/h2\u003e\n\u003ch3 id=\"kullanm-1\"\u003eKullanım\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan gcs \u0026lt;bucket\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eKomut tam olarak bir argüman alır: \u003cstrong\u003ekova adı\u003c/strong\u003e (\u003ccode\u003egs://\u003c/code\u003e öneki olmadan). Tarama hedefi \u003ccode\u003egs://\u0026lt;bucket\u0026gt;\u003c/code\u003e olarak gösterilir.\u003c/p\u003e\n\u003ch3 id=\"kimlik-dorulama-1\"\u003eKimlik doğrulama\u003c/h3\u003e\n\u003cp\u003eLeakwatch \u003ca href=\"https://cloud.google.com/docs/authentication/application-default-credentials\"\u003eApplication Default Credentials (ADC)\u003c/a\u003e kullanır. Kimlik bilgisi arama sırası şu şekildedir:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eHizmet hesabı anahtar dosyasına işaret eden \u003ccode\u003eGOOGLE_APPLICATION_CREDENTIALS\u003c/code\u003e ortam değişkeni.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003egcloud auth application-default login\u003c/code\u003e ile yapılandırılmış kullanıcı kimlik bilgileri.\u003c/li\u003e\n\u003cli\u003eGoogle Compute Engine örneğine, Cloud Run hizmetine veya GKE iş yüküne atanmış hizmet hesabı.\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"gcsye-zg-bayraklar\"\u003eGCS'ye özgü bayraklar\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eTür\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--prefix\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eYalnızca adı bu önekle başlayan nesneleri tara.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--project\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eGCP proje kimliği (bazı ADC yapılandırmalarında gereklidir).\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"gcs-rnekleri\"\u003eGCS örnekleri\u003c/h3\u003e\n\u003cp\u003eBelirli bir GCP projesiyle tüm kovayı tarayın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan gcs my-config-bucket --project my-gcp-project\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eYalnızca belirli bir önek altındaki nesneleri tarayın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan gcs my-bucket --project my-gcp-project --prefix exports/\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eCSV olarak çıktı alın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan gcs my-bucket --format csv --output gcs-results.csv\n\u003c/code\u003e\u003c/pre\u003e\n\u003chr\u003e\n\u003ch2 id=\"ortak-tarama-bayraklar\"\u003eOrtak tarama bayrakları\u003c/h2\u003e\n\u003cp\u003eHem \u003ccode\u003es3\u003c/code\u003e hem de \u003ccode\u003egcs\u003c/code\u003e aynı ortak tarama bayraklarını destekler:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eKısa\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-f\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ejson\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktı biçimi: \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, \u003ccode\u003egithub\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-o\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estdout\u003c/td\u003e\n\u003ctd\u003eSonuçları stdout yerine bu dosyaya yaz.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-c\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCPU sayısı\u003c/td\u003e\n\u003ctd\u003eEşzamanlı çalışan sayısı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--max-file-size\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e10485760\u003c/code\u003e (10 MB)\u003c/td\u003e\n\u003ctd\u003eBu boyutu aşan nesneleri atla (bayt).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--show-raw\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktıda ham sır değerini göster.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eDışlanacak nesne anahtarı kalıbı. Tekrarlanabilir; \u003ccode\u003efilter.exclude-paths\u003c/code\u003e ile birleştirilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-detectors\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eBu çalıştırma için hariç tutulacak dedektör kimlikleri. Tekrarlanabilir; \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e ile birleştirilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--no-verify\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSır doğrulamasını devre dışı bırak.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYalnızca doğrulama ile aktif olduğu onaylanan bulguları raporla.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRaporlanacak minimum önem: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e, \u003ccode\u003ecritical\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--remediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHer bulguya düzeltme rehberi ekle.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eNesne anahtarlarına uygulanan yol dışlamaları \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e dosyasında \u003ccode\u003efilter.exclude-paths\u003c/code\u003e altında ya da her çalıştırma için \u003ccode\u003e--exclude\u003c/code\u003e ile yapılandırılır. \u003ccode\u003e--config\u003c/code\u003e ve \u003ccode\u003e--log-level\u003c/code\u003e (varsayılan \u003ccode\u003ewarn\u003c/code\u003e) kök bayrakları da geçerlidir.\u003c/p\u003e\n\u003ch2 id=\"bulgu-meta-verisi\"\u003eBulgu meta verisi\u003c/h2\u003e\n\u003cp\u003eBir S3 veya GCS bulgusu yalnızca her kaynakta ortak olan alanları ve \u003ccode\u003e\u0026lt;bucket\u0026gt;/\u0026lt;object-key\u0026gt;\u003c/code\u003e olarak ayarlanan \u003ccode\u003efile_path\u003c/code\u003e alanını taşır (bulut depolama bulguları için ayrı bir \u003ccode\u003eline\u003c/code\u003e alanı yoktur).\u003c/p\u003e\n\u003ch2 id=\"k-kodlar\"\u003eÇıkış kodları\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eKod\u003c/th\u003e\n\u003cth\u003eAnlam\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlandı, bulgu yok.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlandı, bulgular raporlandı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama başarısız oldu (kimlik doğrulama hatası, kova bulunamadı, vb.).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlanmadan kesildi (\u003ccode\u003eCtrl+C\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e) ve hiçbir bulgu raporlanmamıştı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eHer çalıştırmanın ardından stderr'e bir tarama özeti yazdırılır. Taramalar SIGINT/SIGTERM sinyalinde düzgün biçimde iptal edilir.\u003c/p\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eHızlı Başlangıç\u003c/a\u003e — ilk taramanızı bir dakikadan kısa sürede çalıştırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e — dışlamaları ve diğer varsayılanları yapılandırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/ignoring-findings\"\u003eBulguları Yoksayma\u003c/a\u003e — bilinen yanlış pozitifleri bastırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eDoğrulama Nasıl Çalışır\u003c/a\u003e — doğrulama durumlarını anlayın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/filesystem\"\u003eDosya Sistemi\u003c/a\u003e — yerel bir dizin ağacını tarayın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Referansı\u003c/a\u003e — tüm komutlar için tam bayrak referansı.\u003c/li\u003e\n\u003c/ul\u003e\n"},"scanning/container-images":{"title":"Konteyner İmajları","description":"Docker daemon gerektirmeksizin OCI ve Docker imaj katmanlarını sızan sırlara karşı tarayın.","html":"\u003ch1 id=\"konteyner-majlar\"\u003eKonteyner İmajları\u003c/h1\u003e\n\u003cp\u003eKonteyner imajları sırların sıklıkla gizlendiği yerlerden biridir: ortam değişkenlerine gömülen API anahtarları, derleme katmanlarına yerleştirilmiş kimlik bilgileri ve imaj katmanlarına kopyalanıp unutulan yapılandırma dosyaları. \u003ccode\u003eleakwatch scan image\u003c/code\u003e, bir OCI veya Docker imajının her katmanını inceler ve bu sırları dağıtım öncesinde gün yüzüne çıkarır.\u003c/p\u003e\n\u003ch2 id=\"temel-kullanm\"\u003eTemel kullanım\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan image \u0026lt;image:tag\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eKomut tam olarak bir argüman alır: standart \u003ccode\u003ename:tag\u003c/code\u003e gösteriminde bir imaj referansı. Leakwatch imajları çekmek ve incelemek için \u003ca href=\"https://github.com/google/go-containerregistry\"\u003ego-containerregistry\u003c/a\u003e kullanır — herhangi bir Docker daemon \u003cstrong\u003egerekmez\u003c/strong\u003e.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Docker Hub imajını tara\nleakwatch scan image nginx:latest\n\n# Özel GitHub Container Registry imajını tara\nleakwatch scan image ghcr.io/org/myapp:v1.2.0\n\n# Amazon ECR imajını tara\nleakwatch scan image 123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"desteklenen-kayt-sunucular\"\u003eDesteklenen kayıt sunucuları\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eKayıt Sunucusu\u003c/th\u003e\n\u003cth\u003eÖrnek referans\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003eDocker Hub\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003enginx:latest\u003c/code\u003e, \u003ccode\u003emyorg/myapp:1.0.0\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eGitHub Container Registry (GHCR)\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003eghcr.io/org/myapp:v1.2.0\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eAmazon ECR\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eGoogle Container Registry (GCR)\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003egcr.io/my-project/myapp:latest\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eOCI uyumlu herhangi bir kayıt sunucusu\u003c/td\u003e\n\u003ctd\u003eStandart \u003ccode\u003eregistry/name:tag\u003c/code\u003e biçimi\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"kimlik-dorulama\"\u003eKimlik doğrulama\u003c/h2\u003e\n\u003cp\u003eLeakwatch, Docker ve diğer OCI araçları tarafından kullanılan standart kimlik bilgisi anahtarlığını kullanır. \u003ccode\u003edocker login\u003c/code\u003e (veya \u003ccode\u003ecrane\u003c/code\u003e, \u003ccode\u003eskopeo\u003c/code\u003e, bulut sağlayıcısı kimlik bilgisi yardımcıları gibi eşdeğer araçlar) ile oturum açtıysanız, Leakwatch bu kimlik bilgilerini otomatik olarak kullanır.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Önce GHCR'a giriş yapın\ndocker login ghcr.io\n\n# Ardından tarayın — kimlik bilgileri otomatik olarak alınır\nleakwatch scan image ghcr.io/org/private-app:latest\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eAmazon ECR için, taramadan önce ECR kimlik bilgisi yardımcısını yapılandırın ya da \u003ccode\u003eAWS_ACCESS_KEY_ID\u003c/code\u003e ve ilgili ortam değişkenlerini ayarlayın.\u003c/p\u003e\n\u003ch2 id=\"tarama-nasl-alr\"\u003eTarama nasıl çalışır\u003c/h2\u003e\n\u003cp\u003eLeakwatch imaj manifestini çeker, her katmanı sırayla işler ve her katmandaki dosyaları çıkarır. Her dosyanın içeriği, dosya sistemi taramasıyla aynı tespit hattından geçirilir. \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e içindeki \u003ccode\u003efilter.exclude-paths\u003c/code\u003e yol dışlamaları (veya \u003ccode\u003e--exclude\u003c/code\u003e) burada da geçerlidir ve katmanlar içinde hangi dosya yollarının inceleneceğini sınırlar.\u003c/p\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNot\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eSıkıştırma bombası katmanlarına karşı bir savunma olarak Leakwatch, okuyacağı kümülatif sıkıştırması açılmış bayt miktarını sınırlar: katman başına 2 GiB ve tüm imaj genelinde 10 GiB. Bir katman veya imaj bu tavanı aşarsa, sıkıştırma açmanın sınırsız çalışmasına izin vermek yerine etkilenen katmanın taraması kısaltılır (bir uyarı günlüğe kaydedilir).\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"bayraklar\"\u003eBayraklar\u003c/h2\u003e\n\u003cp\u003eİmaja özgü bayrak yoktur. Tüm ortak tarama bayrakları geçerlidir:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eKısa\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-f\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ejson\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktı biçimi: \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, \u003ccode\u003egithub\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-o\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estdout\u003c/td\u003e\n\u003ctd\u003eSonuçları stdout yerine bu dosyaya yaz.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-c\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCPU sayısı\u003c/td\u003e\n\u003ctd\u003eEşzamanlı çalışan sayısı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--max-file-size\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e10485760\u003c/code\u003e (10 MB)\u003c/td\u003e\n\u003ctd\u003eBu boyutu aşan dosyaları atla (bayt).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--show-raw\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktıda ham sır değerini göster.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eDışlanacak yol kalıbı. Tekrarlanabilir; \u003ccode\u003efilter.exclude-paths\u003c/code\u003e ile birleştirilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-detectors\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eBu çalıştırma için hariç tutulacak dedektör kimlikleri. Tekrarlanabilir; \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e ile birleştirilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--no-verify\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSır doğrulamasını devre dışı bırak.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYalnızca doğrulama ile aktif olduğu onaylanan bulguları raporla.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRaporlanacak minimum önem: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e, \u003ccode\u003ecritical\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--remediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHer bulguya düzeltme rehberi ekle.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eYol tabanlı dışlamalar \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e dosyasında \u003ccode\u003efilter.exclude-paths\u003c/code\u003e altında ya da her çalıştırma için \u003ccode\u003e--exclude\u003c/code\u003e ile yapılandırılır. Ayrıntılar için \u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e sayfasına bakın.\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003e--config\u003c/code\u003e ve \u003ccode\u003e--log-level\u003c/code\u003e (varsayılan \u003ccode\u003ewarn\u003c/code\u003e) kök bayrakları da geçerlidir.\u003c/p\u003e\n\u003ch2 id=\"rnekler\"\u003eÖrnekler\u003c/h2\u003e\n\u003cp\u003eDocker Hub imajını tarayın ve sonuçları tablo olarak yazdırın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan image alpine:3.20 --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eÖzel kayıt sunucusu imajını tarayın ve SARIF çıktısı kaydedin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan image ghcr.io/org/myapp:v1.2.0 --format sarif -o results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eYalnızca doğrulanmış aktif sırları gösterin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan image myapp:latest --only-verified --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eJSON çıktısına düzeltme rehberi dahil edin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan image myapp:latest --remediation --format json -o image-findings.json\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"bulgu-meta-verisi\"\u003eBulgu meta verisi\u003c/h2\u003e\n\u003cp\u003eİmaj taramasından elde edilen her bulgu katman meta verisi içerir:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eAlan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eimage\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTaranan imaj referansı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003elayer\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBulgunun tespit edildiği katman özeti (imaj yapılandırmasındaki bir bulgu için \u003ccode\u003e\u0026quot;config\u0026quot;\u003c/code\u003e, katman değil).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003elayer_idx\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eİmaj manifestindeki katmanın sayısal indeksi (0 tabanlı); imaj yapılandırmasındaki bir bulgu için \u003ccode\u003e-1\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003efile_path\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eKatman içindeki dosyanın yolu.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eİpucu\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eGizli bilgilerin bir kayıt sunucusuna push edilmeden önce yakalanması için konteyner imaj taramasını CI/CD hattınızın derleme aşamasına entegre edin. Sonuçları doğrudan GitHub Code Scanning'e yüklemek için \u003ccode\u003e--format sarif\u003c/code\u003e kullanın.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"k-kodlar\"\u003eÇıkış kodları\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eKod\u003c/th\u003e\n\u003cth\u003eAnlam\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlandı, bulgu yok.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlandı, bulgular raporlandı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama başarısız oldu (imaj bulunamadı, kimlik doğrulama hatası, vb.).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlanmadan kesildi (\u003ccode\u003eCtrl+C\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e) ve hiçbir bulgu raporlanmamıştı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eHer çalıştırmanın ardından stderr'e bir tarama özeti yazdırılır. Taramalar SIGINT/SIGTERM sinyalinde düzgün biçimde iptal edilir.\u003c/p\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eHızlı Başlangıç\u003c/a\u003e — ilk taramanızı bir dakikadan kısa sürede çalıştırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/filesystem\"\u003eDosya Sistemi\u003c/a\u003e — yerel bir dizin ağacını tarayın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e — dışlamaları ve diğer varsayılanları yapılandırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/ignoring-findings\"\u003eBulguları Yoksayma\u003c/a\u003e — bilinen yanlış pozitifleri bastırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eDoğrulama Nasıl Çalışır\u003c/a\u003e — doğrulama durumlarını anlayın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Referansı\u003c/a\u003e — tüm komutlar için tam bayrak referansı.\u003c/li\u003e\n\u003c/ul\u003e\n"},"scanning/filesystem":{"title":"Dosya Sistemi","description":"leakwatch scan fs komutuyla yerel bir dizin ağacını sızan sırlara karşı tarayın.","html":"\u003ch1 id=\"dosya-sistemi\"\u003eDosya Sistemi\u003c/h1\u003e\n\u003cp\u003eSırlar çoğu zaman önce yerel kaynak kodda ortaya çıkar. \u003ccode\u003eleakwatch scan fs\u003c/code\u003e komutu, bir dizin ağacındaki tüm dosyaları dolaşır, her biri üzerinde tam tespit hattını çalıştırır ve bulguları raporlar — henüz commit edilmeden önce yakalamak ya da mevcut bir kod tabanını sonradan taramak için kullanabilirsiniz.\u003c/p\u003e\n\u003ch2 id=\"temel-kullanm\"\u003eTemel kullanım\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs [path...]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003epath\u003c/code\u003e, \u003cstrong\u003esıfır veya daha fazla\u003c/strong\u003e argüman kabul eder; atlandığında Leakwatch geçerli çalışma dizinini (\u003ccode\u003e.\u003c/code\u003e) tarar. Her yol bir dizin (özyinelemeli olarak dolaşılır) veya tek bir dosya olabilir — tek bir çağrıda dosyaları ve dizinleri karıştırıp birden fazlasını geçebilirsiniz.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Geçerli dizini tara\nleakwatch scan fs\n\n# Belirli bir proje klasörünü tara\nleakwatch scan fs ./my-project\n\n# Tek bir dosyayı tara\nleakwatch scan fs cmd/main.go\n\n# Birden fazla dosya ve dizini birlikte tara\nleakwatch scan fs cmd/ main.go internal/config\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"dosya-sistemi-kaynann-otomatik-olarak-atladklar\"\u003eDosya sistemi kaynağının otomatik olarak atladıkları\u003c/h2\u003e\n\u003cp\u003eTaramaları hızlı ve gürültüsüz tutmak için dosya sistemi kaynağı herhangi bir yapılandırma gerekmeksizin şunları atlar:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eİkili dosyalar\u003c/strong\u003e — dosyanın ilk 8 KB'ında null byte bulunmasıyla tespit edilir.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eBilinen ikili uzantılar\u003c/strong\u003e — yaygın derlenmiş, görsel, ses, video ve arşiv biçimleri.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eKilit dosyaları\u003c/strong\u003e — \u003ccode\u003epackage-lock.json\u003c/code\u003e, \u003ccode\u003eyarn.lock\u003c/code\u003e, \u003ccode\u003ePipfile.lock\u003c/code\u003e ve benzerleri.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003e.git\u003c/code\u003e dizinleri\u003c/strong\u003e — Git nesne/paket deposu asla dolaşılmaz veya düz dosya olarak okunmaz, çünkü commit geçmişini taramanın özel yolu \u003ccode\u003escan git\u003c/code\u003e'tir. Bu, dolaşım sırasında karşılaşılan herhangi bir \u003ccode\u003e.git\u003c/code\u003e dizini için geçerlidir; \u003ccode\u003e.git\u003c/code\u003e'i tarama yolu olarak açıkça verirseniz bu muafiyet uygulanmaz ve normal şekilde taranır. Ayrıca etkilenmeyen bir diğer nokta: döngüleri önlemek için sembolik bağlantılar her zaman atlanır.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"bayraklar\"\u003eBayraklar\u003c/h2\u003e\n\u003ch3 id=\"dosya-sistemine-zg\"\u003eDosya sistemine özgü\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eTür\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring (tekrarlanabilir)\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eDışlanacak yollar için glob desenleri. Birden fazla kez belirtilebilir veya virgülle ayrılabilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"ortak-tarama-bayraklar\"\u003eOrtak tarama bayrakları\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eKısa\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-f\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ejson\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktı biçimi: \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, \u003ccode\u003egithub\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-o\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estdout\u003c/td\u003e\n\u003ctd\u003eSonuçları stdout yerine bu dosyaya yaz.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-c\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCPU sayısı\u003c/td\u003e\n\u003ctd\u003eEşzamanlı çalışan sayısı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--max-file-size\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e10485760\u003c/code\u003e (10 MB)\u003c/td\u003e\n\u003ctd\u003eBu boyutu aşan dosyaları atla (bayt).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--show-raw\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktıda ham sır değerini göster.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-detectors\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eBu çalıştırma için hariç tutulacak dedektör kimlikleri. Tekrarlanabilir; \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e ile birleştirilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--no-verify\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSır doğrulamasını devre dışı bırak.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYalnızca doğrulama ile aktif olduğu onaylanan bulguları raporla.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRaporlanacak minimum önem: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e, \u003ccode\u003ecritical\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--remediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHer bulguya düzeltme rehberi ekle.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003ccode\u003e--config\u003c/code\u003e ve \u003ccode\u003e--log-level\u003c/code\u003e (varsayılan \u003ccode\u003ewarn\u003c/code\u003e) kök bayrakları da geçerlidir.\u003c/p\u003e\n\u003ch2 id=\"rnekler\"\u003eÖrnekler\u003c/h2\u003e\n\u003cp\u003eGeçerli dizini tarayın ve terminalde renklendirilmiş bir tablo yazdırın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eTest dosyalarını ve vendor dizinlerini dışlayıp GitHub Code Scanning için SARIF çıktısı kaydedin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . \\\n --exclude \u0026quot;**/*_test.go\u0026quot; \\\n --exclude \u0026quot;vendor/**\u0026quot; \\\n --format sarif \\\n --output results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBüyük bir monorepo için dosya boyutunu sınırlayın ve çalışan sayısını artırın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --max-file-size 5242880 --concurrency 8 --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eYalnızca yüksek önem dereceli bulguları gösterip rotasyon talimatlarını dahil edin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --min-severity high --remediation --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"yollar-dlama\"\u003eYolları dışlama\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003e--exclude\u003c/code\u003e bayrağı glob desenlerini kabul eder ve birden fazla kez belirtilebilir ya da virgülle ayrılmış liste olarak kullanılabilir:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# İki ayrı bayrak\nleakwatch scan fs . --exclude \u0026quot;**/*_test.go\u0026quot; --exclude \u0026quot;docs/**\u0026quot;\n\n# Virgülle ayrılmış\nleakwatch scan fs . --exclude \u0026quot;**/*_test.go,docs/**\u0026quot;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eTakımınızla paylaşılan kalıcı dışlama kuralları için \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e dosyasına \u003ccode\u003efilter.exclude-paths\u003c/code\u003e altında ekleyin. Bu kurallar yalnızca dosya sistemi taramalarına değil, tüm kaynaklara uygulanır. Proje kök dizininizde bir \u003ccode\u003e.leakwatchignore\u003c/code\u003e dosyası da oluşturabilirsiniz. Ayrıntılar için \u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e ve \u003ca href=\"#/configuration/ignoring-findings\"\u003eBulguları Yoksayma\u003c/a\u003e sayfalarına bakın.\u003c/p\u003e\n\u003ch2 id=\"k-kodlar\"\u003eÇıkış kodları\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eKod\u003c/th\u003e\n\u003cth\u003eAnlam\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlandı, bulgu yok.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlandı, bulgular raporlandı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama başarısız oldu (yapılandırma hatası, okunamayan yol, vb.).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlanmadan kesildi (\u003ccode\u003eCtrl+C\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e) ve hiçbir bulgu raporlanmamıştı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eHer çalıştırmanın ardından stderr'e bir tarama özeti (kaynak türü, hedef, dosya sayısı, süre ve bulgu sayısı) yazdırılır. Taramalar SIGINT/SIGTERM sinyalinde düzgün biçimde iptal edilir.\u003c/p\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eİpucu\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eGeliştirme sırasında \u003ccode\u003eleakwatch scan fs . --format table\u003c/code\u003e komutunu çalıştırarak hızlı bir görsel genel bakış elde edin. CI hatlarında GitHub Code Scanning ile entegrasyon için \u003ccode\u003e--format sarif\u003c/code\u003e seçeneğine geçin.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eHızlı Başlangıç\u003c/a\u003e — ilk taramanızı bir dakikadan kısa sürede çalıştırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e — varsayılan biçimi, dışlamaları ve daha fazlasını yapılandırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/ignoring-findings\"\u003eBulguları Yoksayma\u003c/a\u003e — \u003ccode\u003e.leakwatchignore\u003c/code\u003e ve satır içi baskılama.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eDoğrulama Nasıl Çalışır\u003c/a\u003e — doğrulama durumlarını anlayın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/git-history\"\u003eGit Geçmişi\u003c/a\u003e — çalışma ağacı yerine commit edilmiş geçmişi tarayın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Referansı\u003c/a\u003e — tüm komutlar için tam bayrak referansı.\u003c/li\u003e\n\u003c/ul\u003e\n"},"scanning/git-history":{"title":"Git Geçmişi","description":"Yerel veya uzak bir Git deposunun tüm commit geçmişini sızan sırlara karşı tarayın.","html":"\u003ch1 id=\"git-gemii\"\u003eGit Geçmişi\u003c/h1\u003e\n\u003cp\u003eCommit edilip sonradan silinen bir sır, önceki her commit'te hâlâ mevcuttur ve depoya erişimi olan herkes tarafından ulaşılabilir durumdadır. \u003ccode\u003eleakwatch scan git\u003c/code\u003e, bir deponun — yerel veya uzak — \u003cem\u003etüm\u003c/em\u003e commit geçmişini dolaşarak bu sırları, istismar edilmeden önce gün yüzüne çıkarır.\u003c/p\u003e\n\u003ch2 id=\"temel-kullanm\"\u003eTemel kullanım\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git \u0026lt;url_or_path\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eKomut tam olarak bir argüman alır: depoya giden \u003cstrong\u003eyerel dosya sistemi yolu\u003c/strong\u003e (geçerli dizin için \u003ccode\u003e.\u003c/code\u003e) ya da \u003cstrong\u003euzak HTTP/HTTPS veya SSH URL'si\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eLeakwatch tüm Git işlemleri için \u003ca href=\"https://github.com/go-git/go-git\"\u003ego-git\u003c/a\u003e kullanır; bu, sistem \u003ccode\u003egit\u003c/code\u003e ikili dosyasına bağımlılığı olmayan saf bir Go uygulamasıdır.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003e# Geçerli dizindeki yerel depoyu tara\nleakwatch scan git .\n\n# HTTPS üzerinden uzak bir depoyu tara\nleakwatch scan git https://github.com/org/repo.git\n\n# SSH üzerinden tara\nleakwatch scan git git@github.com:org/repo.git\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"tarama-nasl-alr\"\u003eTarama nasıl çalışır\u003c/h2\u003e\n\u003cp\u003eLeakwatch geçmişteki her commit'i dolaşır ve her commit tarafından eklenen blob'ları inceler. \u003cstrong\u003eBlob-hash tekilleştirmesi\u003c/strong\u003e, aynı dosya içeriğinin kaç commit tarafından referans alındığından bağımsız olarak yalnızca bir kez taranmasını sağlar. Bu, tarama süresini ham commit sayısı yerine depodaki \u003cem\u003ebenzersiz içerik\u003c/em\u003e miktarıyla orantılı tutar.\u003c/p\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNot\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eLeakwatch commit-bazlı diff'leri incelediğinden, sonradan silinen — yani mevcut çalışma ağacında görünmeyen — sırları da bulur.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"bayraklar\"\u003eBayraklar\u003c/h2\u003e\n\u003ch3 id=\"gite-zg\"\u003eGit'e özgü\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eTür\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--since\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring (YYYY-MM-DD)\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eYalnızca bu tarihten sonraki commit'leri tara.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--since-commit\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eYalnızca bu commit hash'inden HEAD'e kadar olan değişiklikleri tara (diff tabanlı).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--branch\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eVarsayılan yerine belirli bir dalı hedef al.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--depth\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eint\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e (tam)\u003c/td\u003e\n\u003ctd\u003e\u003cstrong\u003eYalnızca uzak depolar\u003c/strong\u003e için klonlama derinliği. \u003ccode\u003e0\u003c/code\u003e tam geçmişi tarar.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"ortak-tarama-bayraklar\"\u003eOrtak tarama bayrakları\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eKısa\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-f\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ejson\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktı biçimi: \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, \u003ccode\u003egithub\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-o\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estdout\u003c/td\u003e\n\u003ctd\u003eSonuçları stdout yerine bu dosyaya yaz.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-c\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCPU sayısı\u003c/td\u003e\n\u003ctd\u003eEşzamanlı çalışan sayısı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--max-file-size\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e10485760\u003c/code\u003e (10 MB)\u003c/td\u003e\n\u003ctd\u003eBu boyutu aşan blob'ları atla (bayt).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--show-raw\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktıda ham sır değerini göster.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eDışlanacak yol kalıbı. Tekrarlanabilir; \u003ccode\u003efilter.exclude-paths\u003c/code\u003e ile birleştirilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-detectors\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eBu çalıştırma için hariç tutulacak dedektör kimlikleri. Tekrarlanabilir; \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e ile birleştirilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--no-verify\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSır doğrulamasını devre dışı bırak.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYalnızca doğrulama ile aktif olduğu onaylanan bulguları raporla.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRaporlanacak minimum önem: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e, \u003ccode\u003ecritical\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--remediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHer bulguya düzeltme rehberi ekle.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003ccode\u003e--config\u003c/code\u003e ve \u003ccode\u003e--log-level\u003c/code\u003e (varsayılan \u003ccode\u003ewarn\u003c/code\u003e) kök bayrakları da geçerlidir.\u003c/p\u003e\n\u003ch2 id=\"rnekler\"\u003eÖrnekler\u003c/h2\u003e\n\u003cp\u003eYerel deponun tam geçmişini tarayın ve tablo olarak yazdırın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git . --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003edevelop\u003c/code\u003e dalında belirli bir tarihten sonraki commit'leri tarayın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git . --since 2026-02-23 --branch develop\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBelirli bir commit'ten bu yana tanıtılan değişiklikleri tarayın (CI'da yeni commit'leri kontrol etmek için kullanışlıdır):\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git . --since-commit a1b2c3d\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBüyük bir uzak depoyu hızlandırmak için sığ klonlama yapın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git https://github.com/org/repo.git --depth 50\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eUzak depoyu tarayıp yalnızca doğrulanmış bulguları SARIF olarak kaydedin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git https://github.com/org/repo.git \\\n --only-verified \\\n --format sarif \\\n --output git-results.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"bulgu-meta-verisi\"\u003eBulgu meta verisi\u003c/h2\u003e\n\u003cp\u003eGit taramasından elde edilen her bulgu commit meta verisi içerir:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eAlan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003erepository\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTaranan deponun URL'si veya yolu (kimlik bilgileri ayıklanmış).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecommit\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSırrın tanıtıldığı commit hash'i.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eauthor\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYalnızca commit yazarının adı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eemail\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCommit yazarının e-posta adresi, ayrı bir alan olarak.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edate\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCommit zaman damgası.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ebranch\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDal bağlamı (kullanılabilir olduğunda).\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eİpucu\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003ePull request CI işlerinde yalnızca PR tarafından eklenen commit'leri taramak için \u003ccode\u003e--since-commit\u003c/code\u003e kullanın. Son aktiviteyi kapsayan zamanlanmış gece taramaları için \u003ccode\u003e--since \u0026lt;tarih\u0026gt;\u003c/code\u003e tercih edin.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"kimlik-bilgisi-gvenlii\"\u003eKimlik bilgisi güvenliği\u003c/h2\u003e\n\u003cp\u003eDepo URL'leri gömülü kimlik bilgileri içeriyorsa (örn. \u003ccode\u003ehttps://user:TOKEN@host/repo.git\u003c/code\u003e), Leakwatch bu bilgileri günlüklere veya çıktıya yazmadan önce URL'den ayırır; bu sayede token tarama sonuçlarında veya CI izlerinde hiçbir zaman görünmez.\u003c/p\u003e\n\u003ch2 id=\"k-kodlar\"\u003eÇıkış kodları\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eKod\u003c/th\u003e\n\u003cth\u003eAnlam\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlandı, bulgu yok.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlandı, bulgular raporlandı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama başarısız oldu (geçersiz URL, kimlik doğrulama hatası, vb.).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlanmadan kesildi (\u003ccode\u003eCtrl+C\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e) ve hiçbir bulgu raporlanmamıştı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eHer çalıştırmanın ardından stderr'e bir tarama özeti yazdırılır. Taramalar SIGINT/SIGTERM sinyalinde düzgün biçimde iptal edilir.\u003c/p\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eHızlı Başlangıç\u003c/a\u003e — ilk taramanızı bir dakikadan kısa sürede çalıştırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/multiple-repos\"\u003eÇoklu Depo\u003c/a\u003e — tek komutla birden fazla depoyu tarayın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/filesystem\"\u003eDosya Sistemi\u003c/a\u003e — geçmiş yerine çalışma ağacını tarayın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eDoğrulama Nasıl Çalışır\u003c/a\u003e — doğrulama durumlarını anlayın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/ignoring-findings\"\u003eBulguları Yoksayma\u003c/a\u003e — bilinen yanlış pozitifleri bastırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Referansı\u003c/a\u003e — tüm komutlar için tam bayrak referansı.\u003c/li\u003e\n\u003c/ul\u003e\n"},"scanning/multiple-repos":{"title":"Çoklu Depo","description":"Birden fazla Git deposunu eşzamanlı olarak tarayın ve sonuçları tek bir raporda birleştirin.","html":"\u003ch1 id=\"oklu-depo\"\u003eÇoklu Depo\u003c/h1\u003e\n\u003cp\u003eBir kuruluş büyüdükçe sırlar düzinelerce hatta yüzlerce deponun herhangi birine yerleşebilir. Bunları tek tek kontrol etmek pratik değildir. \u003ccode\u003eleakwatch scan repos\u003c/code\u003e, birden fazla depo URL'sini alır, bunları eşzamanlı olarak tarar ve tüm bulguları tek bir çıktıda birleştirir — tek komut, tek rapor.\u003c/p\u003e\n\u003ch2 id=\"temel-kullanm\"\u003eTemel kullanım\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan repos \u0026lt;url1\u0026gt; \u0026lt;url2\u0026gt; [url...]\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eKomut \u003cstrong\u003een az iki\u003c/strong\u003e depo URL'si gerektirir. Tüm depolar otomatik olarak klonlanır, taranır ve temizlenir. Sonunda birleşik bulgu sayısı ve tek bir tarama özeti raporlanır.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan repos \\\n https://github.com/org/api.git \\\n https://github.com/org/web.git\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"nasl-alr\"\u003eNasıl çalışır\u003c/h2\u003e\n\u003cp\u003eLeakwatch aynı anda en fazla \u003ccode\u003e--parallel\u003c/code\u003e sayıda depo taraması başlatır. Her depo:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eSağlanan URL'den klonlanır (güvenlik açısından kimlik bilgileri günlüklerden ve çıktıdan ayıklanır).\u003c/li\u003e\n\u003cli\u003eTam tespit hattıyla taranır; bu depo için \u003ccode\u003e--concurrency\u003c/code\u003e sayıda çalışan kullanılır.\u003c/li\u003e\n\u003cli\u003eTarama tamamlandığında temizlenir (geçici klon silinir).\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eTüm depolardan elde edilen bulgular toplanır ve tek bir kaynaktan yapılmış tarama gibi tek bir çıktı olarak yazılır. Görüntülenen hedef \u003ccode\u003e\u0026lt;N\u0026gt; repositories\u003c/code\u003e (N depo) şeklindedir.\u003c/p\u003e\n\u003ch2 id=\"bayraklar\"\u003eBayraklar\u003c/h2\u003e\n\u003ch3 id=\"oklu-depoya-zg\"\u003eÇoklu depoya özgü\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eTür\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--parallel\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eint\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eEşzamanlı olarak taranacak depo sayısı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"ortak-tarama-bayraklar\"\u003eOrtak tarama bayrakları\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eKısa\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-f\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ejson\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktı biçimi: \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, \u003ccode\u003egithub\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-o\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estdout\u003c/td\u003e\n\u003ctd\u003eSonuçları stdout yerine bu dosyaya yaz.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-c\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCPU sayısı\u003c/td\u003e\n\u003ctd\u003e\u003cstrong\u003eDepo başına\u003c/strong\u003e eşzamanlı çalışan sayısı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--max-file-size\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e10485760\u003c/code\u003e (10 MB)\u003c/td\u003e\n\u003ctd\u003eBu boyutu aşan blob'ları atla (bayt).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--show-raw\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktıda ham sır değerini göster.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eHer deponun taramasından dışlanacak yol kalıbı. Tekrarlanabilir; \u003ccode\u003efilter.exclude-paths\u003c/code\u003e ile birleştirilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-detectors\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eBu çalıştırma için hariç tutulacak dedektör kimlikleri (örn. \u003ccode\u003eaws-access-key-id\u003c/code\u003e). Tekrarlanabilir; \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e ile birleştirilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--no-verify\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSır doğrulamasını devre dışı bırak.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYalnızca doğrulama ile aktif olduğu onaylanan bulguları raporla.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRaporlanacak minimum önem: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e, \u003ccode\u003ecritical\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--remediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHer bulguya düzeltme rehberi ekle.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003ccode\u003e.leakwatch.yaml\u003c/code\u003e dosyasındaki \u003ccode\u003efilter.exclude-paths\u003c/code\u003e yol dışlamaları (veya \u003ccode\u003e--exclude\u003c/code\u003e) tüm depolara uygulanır. \u003ccode\u003e--config\u003c/code\u003e ve \u003ccode\u003e--log-level\u003c/code\u003e (varsayılan \u003ccode\u003ewarn\u003c/code\u003e) kök bayrakları da geçerlidir.\u003c/p\u003e\n\u003ch2 id=\"rnekler\"\u003eÖrnekler\u003c/h2\u003e\n\u003cp\u003eİki depoyu tarayın ve sonuçları tablo olarak görüntüleyin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan repos \\\n https://github.com/org/api.git \\\n https://github.com/org/web.git \\\n --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBeş depoyu daha yüksek paralellik ile tarayın ve birleşik sonuçları SARIF olarak kaydedin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan repos \\\n https://github.com/org/api.git \\\n https://github.com/org/web.git \\\n https://github.com/org/infra.git \\\n https://github.com/org/mobile.git \\\n https://github.com/org/docs.git \\\n --parallel 4 \\\n --format sarif \\\n --output all-repos.sarif\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eDepo başına daha fazla çalışan kullanarak yalnızca doğrulanmış bulguları gösterin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan repos \\\n https://github.com/org/backend.git \\\n https://github.com/org/frontend.git \\\n --concurrency 8 \\\n --only-verified \\\n --format json \\\n --output verified-findings.json\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"paralellii-ayarlama\"\u003eParalelliği ayarlama\u003c/h2\u003e\n\u003cp\u003eVerimi kontrol eden iki parametre vardır:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e--parallel\u003c/code\u003e, kaç depo klonlama ve taramasının aynı anda çalışacağını kontrol eder. Varsayılan \u003ccode\u003e3\u003c/code\u003e, çoğu iş yükü için uygundur. Ağ bant genişliği ve CPU kapasitesi izin verdiğinde artırın; kısıtlı makinelerde düşürün.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e--concurrency\u003c/code\u003e (\u003ccode\u003e-c\u003c/code\u003e), her bir depodaki dosya blob'larını işleyen çalışan goroutine sayısını kontrol eder. Bu, tüm tarama komutlarında bulunan aynı bayraktır.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eTepe noktasındaki toplam eşzamanlı işlem = \u003ccode\u003e--parallel\u003c/code\u003e × \u003ccode\u003e--concurrency\u003c/code\u003e.\u003c/p\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNot\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eBir veya daha fazla depo taraması başarısız olursa (örneğin ağ hatası veya kimlik doğrulama sorunu nedeniyle), Leakwatch hatayı günlüğe kaydeder ve kalan depoları taramaya devam eder. \u003cstrong\u003eBulgular önceliklidir\u003c/strong\u003e: filtrelemeden sonra tüm çalıştırma genelinde tek bir bulgu bile kalırsa, bazı depoların taraması başarısız olsa dahi çıkış kodu \u003ccode\u003e1\u003c/code\u003e olur. Çıkış kodu \u003ccode\u003e2\u003c/code\u003e (depo tarama hatası) yalnızca genel olarak sıfır bulgu raporlandığında döndürülür.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"kimlik-bilgisi-gvenlii\"\u003eKimlik bilgisi güvenliği\u003c/h2\u003e\n\u003cp\u003eDepo URL'lerindeki gömülü kimlik bilgileri (örn. \u003ccode\u003ehttps://user:TOKEN@host/repo.git\u003c/code\u003e), URL günlüklere, çıktıya veya tarama özetine yazılmadan önce ayıklanır.\u003c/p\u003e\n\u003ch2 id=\"k-kodlar\"\u003eÇıkış kodları\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eKod\u003c/th\u003e\n\u003cth\u003eAnlam\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTüm depo taramaları tamamlandı, bulgu yok.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFiltrelemeden sonra bir veya daha fazla bulgu kaldı — bazı depoların taraması başarısız olsa veya çalıştırma kesintiye uğrasa bile döndürülür; böylece \u0026quot;sır bulundu\u0026quot; sinyali ilgisiz bir hata tarafından hiçbir zaman gizlenmez.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSıfır bulgu raporlandı ve bir veya daha fazla depo taraması başarısız oldu, ya da bir yapılandırma hatası oluştu.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇalıştırma tamamlanmadan kesildi (\u003ccode\u003eCtrl+C\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e) ve sıfır bulgu raporlanmıştı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eHer çalıştırmanın ardından stderr'e bir tarama özeti yazdırılır. Taramalar SIGINT/SIGTERM sinyalinde düzgün biçimde iptal edilir.\u003c/p\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/git-history\"\u003eGit Geçmişi\u003c/a\u003e — tek bir depoyu derinlemesine tarayın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eHızlı Başlangıç\u003c/a\u003e — ilk taramanızı bir dakikadan kısa sürede çalıştırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e — tüm kaynaklar için paylaşılan varsayılanları yapılandırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/ignoring-findings\"\u003eBulguları Yoksayma\u003c/a\u003e — bilinen yanlış pozitifleri bastırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eDoğrulama Nasıl Çalışır\u003c/a\u003e — doğrulama durumlarını anlayın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Referansı\u003c/a\u003e — tüm komutlar için tam bayrak referansı.\u003c/li\u003e\n\u003c/ul\u003e\n"},"scanning/slack":{"title":"Slack Çalışma Alanı","description":"Slack mesajlarını ve isteğe bağlı metin eklerini sızan sırlara karşı tarayın.","html":"\u003ch1 id=\"slack-alma-alan\"\u003eSlack Çalışma Alanı\u003c/h1\u003e\n\u003cp\u003eGeliştiriciler çoğu zaman kimlik bilgilerini sohbet üzerinden paylaşır — hızlı bir test için bir kanala yapıştırılan token, DM ile gönderilen parola ya da olay başlığına yüklenen bir yapılandırma dosyası. \u003ccode\u003eleakwatch scan slack\u003c/code\u003e, Slack çalışma alanınızdaki mesaj metinlerini ve açıkça etkinleştirildiğinde metin benzeri dosya eklerini okur.\u003c/p\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNot\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eDosya eki taraması isteğe bağlıdır. \u003ccode\u003e--include-files\u003c/code\u003e kullanın ve \u003ccode\u003efiles:read\u003c/code\u003e kapsamını verin; bayrak olmadan Leakwatch hiçbir eki indirmez.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"temel-kullanm\"\u003eTemel kullanım\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan slack\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBu komut \u003cstrong\u003ekonumsal argüman almaz\u003c/strong\u003e. Tüm yapılandırma bayraklar veya ortam değişkenleri aracılığıyla sağlanır.\u003c/p\u003e\n\u003ch2 id=\"kimlik-dorulama\"\u003eKimlik doğrulama\u003c/h2\u003e\n\u003cp\u003eBir Slack Bot Token gereklidir. \u003ccode\u003e--token\u003c/code\u003e bayrağı veya \u003ccode\u003eLEAKWATCH_SLACK_TOKEN\u003c/code\u003e ortam değişkeni aracılığıyla sağlayın. Ortam değişkeni kullanmak önerilir; böylece token kabuk geçmişinde veya süreç listelerinde asla görünmez.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eexport LEAKWATCH_SLACK_TOKEN=xoxb-...\nleakwatch scan slack\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"gerekli-bot-token-kapsamlar\"\u003eGerekli bot token kapsamları\u003c/h3\u003e\n\u003cp\u003eBot token'ı, aşağıdaki OAuth kapsamlarına sahip bir Slack uygulamasıyla ilişkilendirilmiş olmalıdır:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eKapsam\u003c/th\u003e\n\u003cth\u003eAmaç\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003echannels:read\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGenel kanalları listele.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003echannels:history\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBotun katıldığı genel kanallardaki mesajları oku.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egroups:read\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÖzel kanalları listele.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egroups:history\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBotun katıldığı özel kanallardaki mesajları oku.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eim:read\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDoğrudan mesaj konuşmalarını listele (yalnızca \u003ccode\u003e--include-dms\u003c/code\u003e ile gerekli).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eim:history\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDoğrudan mesajları oku (yalnızca \u003ccode\u003e--include-dms\u003c/code\u003e ile gerekli).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003empim:read\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGrup doğrudan mesaj konuşmalarını listele (yalnızca \u003ccode\u003e--include-dms\u003c/code\u003e ile gerekli).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003empim:history\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGrup doğrudan mesajlarını oku (yalnızca \u003ccode\u003e--include-dms\u003c/code\u003e ile gerekli).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003efiles:read\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDosya meta verisini ve içeriğini oku (yalnızca \u003ccode\u003e--include-files\u003c/code\u003e ile gerekli).\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"bayraklar\"\u003eBayraklar\u003c/h2\u003e\n\u003ch3 id=\"slacke-zg\"\u003eSlack'e özgü\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eTür\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eSlack Bot Token. \u003ccode\u003eLEAKWATCH_SLACK_TOKEN\u003c/code\u003e ortam değişkeni tercih edilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--channels\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003etüm kanallar\u003c/td\u003e\n\u003ctd\u003eTaranacak kanal adlarının virgülle ayrılmış listesi.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-channels\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eAtlanacak kanal adlarının virgülle ayrılmış listesi.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--since\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estring (YYYY-MM-DD)\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eBu tarihte veya sonrasında gönderilen mesajları tara.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--include-dms\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ebool\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDoğrudan mesajları ve grup DM'lerini de tara.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--include-files\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ebool\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBoyutu sınırlı metin benzeri dosya eklerini indir ve tara. \u003ccode\u003efiles:read\u003c/code\u003e gerektirir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--rate-limit\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003efloat\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eİsteğe bağlı, işlem başına saniyelik Slack istek üst sınırı. Sıfır, güvenli işlem varsayılanlarını korur: history \u003ccode\u003e1/60\u003c/code\u003e, liste/dosya/indirme sınırları daha yüksektir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"ortak-tarama-bayraklar\"\u003eOrtak tarama bayrakları\u003c/h3\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBayrak\u003c/th\u003e\n\u003cth\u003eKısa\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--format\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-f\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ejson\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktı biçimi: \u003ccode\u003ejson\u003c/code\u003e, \u003ccode\u003esarif\u003c/code\u003e, \u003ccode\u003ecsv\u003c/code\u003e, \u003ccode\u003etable\u003c/code\u003e, \u003ccode\u003egithub\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--output\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-o\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003estdout\u003c/td\u003e\n\u003ctd\u003eSonuçları stdout yerine bu dosyaya yaz.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--concurrency\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e-c\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCPU sayısı\u003c/td\u003e\n\u003ctd\u003eEşzamanlı çalışan sayısı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--max-file-size\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e10485760\u003c/code\u003e (10 MB)\u003c/td\u003e\n\u003ctd\u003eTarama için belleğe alınan en büyük ek boyutu (bayt).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--show-raw\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇıktıda ham sır değerini göster.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--exclude-detectors\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003eBu çalıştırma için hariç tutulacak dedektör kimlikleri. Tekrarlanabilir; \u003ccode\u003efilter.exclude-detectors\u003c/code\u003e ile birleştirilir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--no-verify\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSır doğrulamasını devre dışı bırak.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--only-verified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYalnızca doğrulama ile aktif olduğu onaylanan bulguları raporla.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--min-severity\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003elow\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRaporlanacak minimum önem: \u003ccode\u003elow\u003c/code\u003e, \u003ccode\u003emedium\u003c/code\u003e, \u003ccode\u003ehigh\u003c/code\u003e, \u003ccode\u003ecritical\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e--remediation\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e—\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003efalse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHer bulguya düzeltme rehberi ekle.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003ccode\u003e--config\u003c/code\u003e ve \u003ccode\u003e--log-level\u003c/code\u003e (varsayılan \u003ccode\u003ewarn\u003c/code\u003e) kök bayrakları da geçerlidir. Diğer her tarama alt komutunun aksine, \u003ccode\u003escan slack\u003c/code\u003e komutunda \u003ccode\u003e--exclude\u003c/code\u003e yol-kalıbı bayrağı yoktur — bunun yerine tüm kanalları atlamak için \u003ccode\u003e--exclude-channels\u003c/code\u003e kullanın.\u003c/p\u003e\n\u003ch2 id=\"rnekler\"\u003eÖrnekler\u003c/h2\u003e\n\u003cp\u003eToken için ortam değişkeni kullanarak botun erişebildiği tüm kanalları tarayın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eexport LEAKWATCH_SLACK_TOKEN=xoxb-...\nleakwatch scan slack\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBelirli kanalları tarayın ve yılın başından bu yana gönderilen mesajlarla sınırlayın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan slack \\\n --channels general,engineering,backend \\\n --since 2026-01-01\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eGürültülü kanalları dışlayın ve doğrudan mesajları dahil edin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan slack \\\n --exclude-channels random,social,giphy \\\n --include-dms\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eDosya başına bellek sınırını düşürerek metin benzeri ekleri dahil edin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan slack \\\n --include-files \\\n --max-file-size 5242880\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eAPI istek hızını yalnız yayımlanmış katmanı izin veren Marketplace/dahili bir uygulama için artırın:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan slack --rate-limit 0.8 --format table\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eYalnızca doğrulanmış aktif bulguları bir JSON dosyasına kaydedin:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan slack \\\n --only-verified \\\n --format json \\\n --output slack-findings.json\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 id=\"bulgu-meta-verisi\"\u003eBulgu meta verisi\u003c/h2\u003e\n\u003cp\u003eSlack taramasından elde edilen her bulgu mesaj ve kanal meta verisi içerir:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eAlan\u003c/th\u003e\n\u003cth\u003eAçıklama\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003echannel\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBulgunun tespit edildiği Slack kanalının \u003cstrong\u003ekimliği\u003c/strong\u003e (örn. \u003ccode\u003eC0123456\u003c/code\u003e) — okunabilir ad değil.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003echannel_name\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003echannel\u003c/code\u003e alanından ayrı, okunabilir kanal adı (örn. \u003ccode\u003eengineering\u003c/code\u003e).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003emessage_user\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMesaj yazarının Slack kullanıcı kimliği. Slack bulguları için \u003ccode\u003eauthor\u003c/code\u003e alanı yoktur.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003emessage_ts\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSlack mesaj zaman damgası (benzersiz mesaj kimliği).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ethread_ts\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYalnızca bulgu bir ileti dizisi yanıtındaysa bulunan, üst mesajın zaman damgası.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003efile_path\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eEkten gelen bulgularda bulunan sentetik \u003ccode\u003eslack/\u0026lt;kanal\u0026gt;/\u0026lt;dosya-adı\u0026gt;\u003c/code\u003e yolu.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"performans-deerlendirmeleri\"\u003ePerformans değerlendirmeleri\u003c/h2\u003e\n\u003cp\u003eSlack API sınırları yöntem, çalışma alanı ve uygulama başına uygulanır. Bu nedenle Leakwatch bağımsız limiter kovaları kullanır: \u003ccode\u003econversations.history\u003c/code\u003e, yeni dağıtılan Marketplace dışı uygulamalar için varsayılan dakikada bir istektir; \u003ccode\u003econversations.list\u003c/code\u003e Tier 2'yi (20+/dakika), \u003ccode\u003efiles.info\u003c/code\u003e Tier 4'ü (100+/dakika) izler ve ek indirmelerinin ayrı, ihtiyatlı bir 100/dakika istemci sınırı vardır. \u003ccode\u003e--rate-limit\u003c/code\u003e her kovayı aynı işlem-başı üst sınırla açıkça geçersiz kılar; Tier 3 erişimli Marketplace/dahili uygulamalar bunu bilinçli olarak artırabilir.\u003c/p\u003e\n\u003cp\u003eSlack \u003ccode\u003e429 Too Many Requests\u003c/code\u003e ile yanıt verdiğinde, Leakwatch \u003ccode\u003eRetry-After\u003c/code\u003e başlığına otomatik olarak uyar ve taramayı tamamen başarısız kılmak yerine isteği yeniden dener.\u003c/p\u003e\n\u003cp\u003eDosya meta verisi ve indirmeler birbirlerinin veya history kapasitesini tüketmek yerine ayrı limiter kovaları kullanır. Leakwatch yalnızca Slack'e ait HTTPS indirme URL'lerini kabul eder, ek chunk'larını doğrudan backpressure ile aktarır, en fazla \u003ccode\u003e--max-file-size\u003c/code\u003e kadar veriyi belleğe alır, MIME boş ya da yanıltıcı olsa bile geçersiz UTF-8/ikili içeriği reddeder ve aynı Slack dosya kimliğini mesajlar arasında tekilleştirir.\u003c/p\u003e\n\u003cp\u003eHer çalıştırmada tüm çalışma alanını taramak yerine belirli kanalları hedeflemek için \u003ccode\u003e--channels\u003c/code\u003e kullanın. Mesajları artımlı biçimde taramak için \u003ccode\u003e--since\u003c/code\u003e ile birleştirin.\u003c/p\u003e\n\u003ch2 id=\"k-kodlar\"\u003eÇıkış kodları\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eKod\u003c/th\u003e\n\u003cth\u003eAnlam\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e0\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlandı, bulgu yok.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e1\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlandı, bulgular raporlandı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e2\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama başarısız oldu (eksik token, kimlik doğrulama hatası, vb.).\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003e3\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTarama tamamlanmadan kesildi (\u003ccode\u003eCtrl+C\u003c/code\u003e / \u003ccode\u003eSIGTERM\u003c/code\u003e) ve hiçbir bulgu raporlanmamıştı.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eHer çalıştırmanın ardından stderr'e bir tarama özeti yazdırılır. Taramalar SIGINT/SIGTERM sinyalinde düzgün biçimde iptal edilir.\u003c/p\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/getting-started/quick-start\"\u003eHızlı Başlangıç\u003c/a\u003e — ilk taramanızı bir dakikadan kısa sürede çalıştırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eYapılandırma Dosyası\u003c/a\u003e — \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e ile varsayılanları yapılandırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/ignoring-findings\"\u003eBulguları Yoksayma\u003c/a\u003e — bilinen yanlış pozitifleri bastırın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eDoğrulama Nasıl Çalışır\u003c/a\u003e — doğrulama durumlarını anlayın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/scanning/git-history\"\u003eGit Geçmişi\u003c/a\u003e — commit edilmiş geçmişi sırlara karşı tarayın.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/reference/cli-reference\"\u003eCLI Referansı\u003c/a\u003e — tüm komutlar için tam bayrak referansı.\u003c/li\u003e\n\u003c/ul\u003e\n"},"verification/how-verification-works":{"title":"Doğrulama Nasıl Çalışır","description":"Leakwatch'ın tespit edilen bir sırrın hâlâ aktif olup olmadığını nasıl teyit ettiği, hangi doğrulama modlarını kullandığı ve doğrulamanın nasıl yapılandırılacağı veya devre dışı bırakılacağı.","html":"\u003ch1 id=\"dorulama-nasl-alr\"\u003eDoğrulama Nasıl Çalışır\u003c/h1\u003e\n\u003cp\u003eBir kod tabanında sır bulmak hikayenin yalnızca yarısıdır. Altı ay önce döndürülen bir anahtar gürültüdür; hâlâ canlı olan bir anahtar ise aktif bir olayı temsil eder. Doğrulama, bu çizgiyi çizen adımdır — tespit edilen her bulguyu alır ve mümkün olan durumlarda sırrın sağlayıcıda hâlâ geçerli olup olmadığını teyit eder.\u003c/p\u003e\n\u003ch2 id=\"tespiten-dorulamaya\"\u003eTespiten doğrulamaya\u003c/h2\u003e\n\u003cp\u003eTarama motoru bulguları topladıktan sonra doğrulayıcı havuzu onları işlemeye alır. Her bulgu bir \u003ccode\u003edetector_id\u003c/code\u003e taşır; Leakwatch bu ID için kayıtlı bir doğrulayıcı olup olmadığını arar:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eBir doğrulayıcı mevcutsa çalışır ve bir durum döndürür.\u003c/li\u003e\n\u003cli\u003eO dedektör türü için kayıtlı bir doğrulayıcı yoksa bulgu değiştirilmeden \u003ccode\u003eunverified\u003c/code\u003e durumuyla geçer.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"-dorulama-modu\"\u003eÜç doğrulama modu\u003c/h2\u003e\n\u003cp\u003eTüm sırlar aynı şekilde doğrulanamaz. Leakwatch doğrudan canlı kontrolleri, güvenilir veya eşlik eden bağlam gerektiren kontrolleri ve çevrimdışı format doğrulamasını birbirinden ayırır. Bu nedenle registry kaydı canlı kabiliyetin kanıtı sayılmaz.\u003c/p\u003e\n\u003ch3 id=\"canl-api-dorulamas\"\u003eCanlı API doğrulaması\u003c/h3\u003e\n\u003cp\u003e39 dedektör türü için Leakwatch normal üretim yolunda \u003cstrong\u003ekontrollü, yıkıcı olmayan bir sağlayıcı kontrolü\u003c/strong\u003e yapabilir — örneğin AWS anahtarları için \u003ccode\u003ests:GetCallerIdentity\u003c/code\u003e, OpenAI anahtarları için sabit sağlayıcı kimlik uç noktası. Çağrı yalnızca kimliği doğrulamak için gereken minimum uç noktayı kullanır; hiçbir zaman veri değiştirmez veya kaynak oluşturmaz, ancak sağlayıcı kotası tüketebilir.\u003c/p\u003e\n\u003cp\u003eSağlayıcı sözleşmeye uygun başarılı bir yanıt döndürürse bulgu \u003ccode\u003everified_active\u003c/code\u003e olarak işaretlenir. Bir bulgu yalnızca sağlayıcı yanıtı ilgili doğrulayıcı sözleşmesine göre kesin olduğunda \u003ccode\u003everified_inactive\u003c/code\u003e olur. İzin reddi ve belirsiz yanıtlar \u003ccode\u003everify_error\u003c/code\u003e olarak kalır; örneğin SendGrid yalnız HTTP \u003ccode\u003e401\u003c/code\u003e yanıtını inaktif sayar, \u003ccode\u003e403\u003c/code\u003e ise sonuçsuzdur.\u003c/p\u003e\n\u003ch3 id=\"gvenilir-veya-elik-eden-balam-gerekli\"\u003eGüvenilir veya eşlik eden bağlam gerekli\u003c/h3\u003e\n\u003cp\u003eKayıtlı dokuz implementasyon çıplak dedektör bulgusundan güvenli canlı istek yapamaz. Auth0, GitLab, Grafana, GitHub/GHES, Datadog ve Snyk güvenilir issuer/site/API origin'i gerektirir. Twilio bulgusu açıkça eşleşen API Key Secret ile gizli olmayan Key SID'yi içerir, ancak yine de güvenilir bölgesel origin gerekir. Shopify için operatörce güvenilen mağaza origin'i zorunludur. Bu hedefleri yalnızca tekrarlanabilir \u003ccode\u003e--verifier-origin detector-id=https://host\u003c/code\u003e komut satırı bayrağıyla verin (\u003ccode\u003e--grafana-instance-url\u003c/code\u003e Grafana için korunmuştur). Proje yapılandırması, ortam değişkenleri, repo URL'leri ve token iddiaları doğrulama hedefi seçemez. Açık bağlam olmadan Leakwatch istek göndermez ve \u003ccode\u003eunverified\u003c/code\u003e döndürür.\u003c/p\u003e\n\u003ch3 id=\"yalnzca-format-dorulamas\"\u003eYalnızca format doğrulaması\u003c/h3\u003e\n\u003cp\u003eAltı kimlik bilgisi türü için güvenli bir canlı kontrol mevcut değildir — sağlayıcının anonim bir kimlik uç noktası yoktur, gerçek bir çağrı yan etkiye yol açar ya da (\u003ccode\u003ecoinbase-api-key\u003c/code\u003e için) canlı API, anahtarla güvenilir biçimde ilişkilendirilemeyen eşleştirilmiş bir sırla HMAC istek imzalaması gerektirir. Bu durumlar için Leakwatch, herhangi bir ağ isteği yapmadan kimlik bilgisinin yapısını doğrular:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eDedektör ID\u003c/th\u003e\n\u003cth\u003eDoğrulanan özellik\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egcp-service-account\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eJSON yapısı — \u003ccode\u003etype\u003c/code\u003e, \u003ccode\u003eproject_id\u003c/code\u003e, \u003ccode\u003eprivate_key_id\u003c/code\u003e, \u003ccode\u003eclient_email\u003c/code\u003e alanlarının varlığı\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003erabbitmq-connection-string\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAMQP URL'nin başarıyla ayrıştırılması\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esnowflake-credentials\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eYalnızca format kontrolü — geçerli bir format hiçbir şeyi kanıtlamaz, sonuç her zaman \u003ccode\u003eunverified\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eazure-storage-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFormat kontrolü\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eazure-entra-secret\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFormat kontrolü\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecoinbase-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eKarakter kümesi ve uzunluk kontrolü\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNot\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eFormat kontrolü geçse bile sonuç \u003ccode\u003eunverified\u003c/code\u003e olarak kalır. Yapısal olarak geçerli bir kimlik bilgisi süresi dolmuş veya iptal edilmiş olabilir. Bu bulgular her zaman manuel inceleme gerektirir.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"dorulama-durumlar\"\u003eDoğrulama durumları\u003c/h2\u003e\n\u003cp\u003eLeakwatch çıktısındaki her bulgu dört durumdan birini taşır:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eDurum\u003c/th\u003e\n\u003cth\u003eAnlam\u003c/th\u003e\n\u003cth\u003eÖnerilen eylem\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003everified_active\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSırrın sağlayıcı tarafından canlı olduğu teyit edildi.\u003c/td\u003e\n\u003ctd\u003eAktif bir olay olarak ele alın. Hemen döndürün.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003everified_inactive\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSağlayıcı kimlik bilgisini reddetti.\u003c/td\u003e\n\u003ctd\u003eMuhtemelen zaten döndürülmüş. Bağlamı gözden geçirin ve kapatın.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eunverified\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDoğrulayıcı yoktur, gerekli bağlam eksiktir, yalnız-format doğrulayıcısı canlılığı kanıtlayamaz veya doğrulama devre dışıdır.\u003c/td\u003e\n\u003ctd\u003eManuel olarak inceleyin; risk bağlama göre belirlenir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003everify_error\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDoğrulayıcı çalıştı ancak ağ hatası, zaman aşımı veya beklenmedik yanıtla karşılaştı.\u003c/td\u003e\n\u003ctd\u003ePotansiyel olarak aktif kabul edin. Yeniden deneyin veya manuel olarak inceleyin.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"dorulama-motoru\"\u003eDoğrulama motoru\u003c/h2\u003e\n\u003cp\u003eDoğrulama, tarama çalışan havuzundan yalıtılmış ayrı bir eşzamanlı çalışan havuzunda çalışır. Sağlayıcı hız sınırlarını tetiklememek için varsayılanlar temkinlidir:\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eAyar\u003c/th\u003e\n\u003cth\u003eVarsayılan\u003c/th\u003e\n\u003cth\u003eYapılandırma anahtarı\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003eÇalışan sayısı\u003c/td\u003e\n\u003ctd\u003e4\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003everification.concurrency\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eGlobal + dedektör başına istek tavanları\u003c/td\u003e\n\u003ctd\u003eHer biri 10 istek/saniye\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003everification.rate-limit\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eBulgu başına tam doğrulama operasyonu zaman aşımı\u003c/td\u003e\n\u003ctd\u003e10 sn\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003everification.timeout\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eHer üç değer de \u003ccode\u003e.leakwatch.yaml\u003c/code\u003e içindeki \u003ccode\u003everification:\u003c/code\u003e bloğu altında ayarlanabilir:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-yaml\"\u003everification:\n enabled: true\n concurrency: 4\n rate-limit: 10.0 # global ve dedektör başına, saniye başına istek sayısı\n timeout: 10s\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-tip\"\u003e\u003cdiv class=\"callout-label\"\u003eİpucu\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003eYüzlerce bulgu tetikleyen depolarda sağlayıcı için güvenli hız tavanını koruyun; daha yumuşak trafik gerekiyorsa \u003ccode\u003econcurrency\u003c/code\u003e değerini düşürün. \u003ccode\u003e--only-verified\u003c/code\u003e yalnız çıktı filtrelemesini değiştirir, doğrulama isteği sayısını azaltmaz.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003cp\u003eAdmission yalnızca gerçek bir sağlayıcı isteğinin hemen öncesinde yapılır. Yalnız-format ve eksik-bağlam sonuçları sınırlayıcı kapasitesi tüketmez. Dedektör başına kova o dedektörün kendi istek hızını, paylaşılan global kova ise toplam trafiği sınırlar; bu yapı sağlayıcılar arasında adil zamanlama garantisi vermez.\u003c/p\u003e\n\u003ch2 id=\"komut-satrndan-dorulamay-kontrol-etme\"\u003eKomut satırından doğrulamayı kontrol etme\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003e--no-verify\u003c/code\u003e ile \u003cstrong\u003edoğrulamayı tamamen devre dışı bırakın\u003c/strong\u003e (ya da yapılandırmada \u003ccode\u003everification.enabled: false\u003c/code\u003e ayarlayın). Her bulgu \u003ccode\u003eunverified\u003c/code\u003e olarak geçer. Bunu çevrimdışı veya hava boşluklu ortamlar için ya da herhangi bir sağlayıcı API'sine dokunmadan mümkün olan en hızlı taramayı istediğinizde kullanın.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan fs . --no-verify\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003cstrong\u003eYalnızca canlı olduğu doğrulanan sırları görmek\u003c/strong\u003e için \u003ccode\u003e--only-verified\u003c/code\u003e kullanın. \u003ccode\u003everified_active\u003c/code\u003e olmayan her şey çıktıdan düşürülür. Bu, büyük bir sonuç kümesini önceliklendirmenin en hızlı yoludur — yalnızca hemen harekete geçmeniz gereken anahtarları görürsünüz.\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003eleakwatch scan git . --only-verified\n\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"callout callout-warn\"\u003e\u003cdiv class=\"callout-label\"\u003eUyarı\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003e\u003ccode\u003e--only-verified\u003c/code\u003e, \u003ccode\u003eunverified\u003c/code\u003e ve \u003ccode\u003everify_error\u003c/code\u003e bulgularını sessizce düşürür. Bunu uyumluluk bağlamında tek filtreniz olarak kullanmayın — bazı kimlik bilgisi türleri (JWT'ler, genel API anahtarları, özel anahtarlar) hiçbir zaman doğrulanamaz ve her zaman dışarıda kalır.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"sr-gvenlii\"\u003eSır güvenliği\u003c/h2\u003e\n\u003cp\u003eDoğrulama, ham sır değerinin süreç sınırını güvensiz biçimde asla terk etmeyecek şekilde tasarlanmıştır:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eDoğrulayıcılar sırrı TLS üzerinden doğrudan sağlayıcının HTTP uç noktasına iletir — diske yazılmaz, bir loga gönderilmez ve çalıştırmalar arasında önbelleğe alınmaz.\u003c/li\u003e\n\u003cli\u003eBaşlatılamayan veya panikle karşılaşan bir doğrulayıcı motor tarafından yakalanır; motor, bulguyu \u003ccode\u003everify_error\u003c/code\u003e olarak işaretler ve taramayı çökertmeden devam eder.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/verification/verification-coverage\"\u003eDoğrulama Kapsamı\u003c/a\u003e — hangi dedektör türlerinin canlı doğrulandığı, format doğrulandığı veya hiç doğrulanamadığı.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/configuration/config-file\"\u003eYapılandırma: Yapılandırma Dosyası\u003c/a\u003e — \u003ccode\u003everification:\u003c/code\u003e bloğunun tam referansı.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/output/output-formats\"\u003eÇıktı Formatları\u003c/a\u003e — doğrulama durumunun JSON, SARIF, CSV ve tablo çıktısında nasıl göründüğü.\u003c/li\u003e\n\u003c/ul\u003e\n"},"verification/verification-coverage":{"title":"Doğrulama Kapsamı","description":"65 yerleşik dedektörün hangilerinin canlı doğrulandığı, bağlam gerektirdiği, yalnızca format doğruladığı veya doğrulanamaz olduğu ve bunun önceliklendirme açısından ne anlama geldiği.","html":"\u003ch1 id=\"dorulama-kapsam\"\u003eDoğrulama Kapsamı\u003c/h1\u003e\n\u003cp\u003eLeakwatch \u003cstrong\u003e65 yerleşik dedektör\u003c/strong\u003e ve kayıtlı 54 doğrulayıcı implementasyonuyla gelir. Registry kaydı canlı kabiliyetle aynı şey değildir: \u003cstrong\u003e39\u003c/strong\u003e dedektör normal üretim yolunda canlı kontrol yapabilir, \u003cstrong\u003e9\u003c/strong\u003e dedektör güvenilir operatör veya eşlik eden bağlam gerektirir, \u003cstrong\u003e6\u003c/strong\u003e dedektör yalnızca çevrimdışı format doğrular ve \u003cstrong\u003e11\u003c/strong\u003e dedektörün doğrulayıcısı yoktur. Bu sayfa her dedektörü gerçek doğrulama sözleşmesine göre eşler.\u003c/p\u003e\n\u003ch2 id=\"canl-dorulanan-39-dedektr-tr\"\u003eCanlı doğrulanan (39 dedektör türü)\u003c/h2\u003e\n\u003cp\u003eBu türler için Leakwatch normal üretim yolunda kontrollü, yıkıcı olmayan bir sağlayıcı kontrolü yapabilir. Sözleşmeye uygun başarı \u003ccode\u003everified_active\u003c/code\u003e döndürebilir; yalnızca doğru issuer üzerindeki kesin kimlik doğrulama reddi \u003ccode\u003everified_inactive\u003c/code\u003e döndürebilir. Belirsiz yanıtlar \u003ccode\u003everify_error\u003c/code\u003e kalır.\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eDedektör türü\u003c/th\u003e\n\u003cth\u003eSağlayıcı\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eaws-access-key-id\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAWS STS (\u003ccode\u003eGetCallerIdentity\u003c/code\u003e)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eslack-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSlack Web API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eopenai-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOpenAI API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eanthropic-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAnthropic API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edeepseek-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDeepSeek API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ehuggingface-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHugging Face API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esendgrid-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSendGrid Web API (\u003ccode\u003e401\u003c/code\u003e inaktiftir; izin reddi dâhil \u003ccode\u003e403\u003c/code\u003e, \u003ccode\u003everify_error\u003c/code\u003e olarak kalır)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003emailgun-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMailgun API (doğru AB veya ABD bölgesel uç noktasını otomatik olarak tespit edip çağırır)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epostmark-server-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePostmark API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003estripe-api-key-live\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eStripe API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003estripe-api-key-test\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eStripe API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edigitalocean-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDigitalOcean API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecloudflare-api-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCloudflare API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eheroku-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eHeroku Platform API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003evercel-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eVercel REST API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003enpm-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003enpm Registry API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epypi-api-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePyPI API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003erubygems-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eRubyGems API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edockerhub-pat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDocker Hub API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecircleci-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eCircleCI API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eterraform-cloud-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTerraform Cloud API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ediscord-bot-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDiscord API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003etelegram-bot-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTelegram Bot API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esentry-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSentry API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003epagerduty-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003ePagerDuty API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003enewrelic-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNew Relic NerdGraph (resmî ABD/AB uçlarında sınırlı fallback; yalnızca iki bölge de \u003ccode\u003e401\u003c/code\u003e döndürürse inaktif)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edoppler-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDoppler API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003elaunchdarkly-sdk-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eLaunchDarkly API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esonarcloud-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSonarCloud API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003enotion-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eNotion API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003elinear-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eLinear API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003efigma-pat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFigma REST API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eairtable-pat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAirtable API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eokta-api-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOkta API (token ile birlikte yakalanan organizasyon alan adını hedefler)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edatabricks-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eDatabricks REST API (token ile birlikte yakalanan çalışma alanı ana bilgisayarını çağırır)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ebitbucket-app-password\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBitbucket REST API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esupabase-service-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eSupabase Management API (\u003ccode\u003esbp_\u003c/code\u003e kişisel erişim token'ı; \u003ccode\u003e401\u003c/code\u003e inaktiftir, \u003ccode\u003e403\u003c/code\u003e \u003ccode\u003everify_error\u003c/code\u003e kalır)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003einfura-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eInfura API\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eteams-webhook\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eMicrosoft Teams\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"gvenilir-veya-elik-eden-balam-gerektiren-9-dedektr-tr\"\u003eGüvenilir veya eşlik eden bağlam gerektiren (9 dedektör türü)\u003c/h2\u003e\n\u003cp\u003eBu implementasyonlar registry'de kayıtlıdır; ancak çıplak bir dedektör bulgusu güvenli issuer seçmek veya doğrulama isteğinde kimlik doğrulamak için yeterli değildir. Gerekli bağlam yoksa Leakwatch güvenli olmayan bir varsayım yapmaz ve \u003ccode\u003eunverified\u003c/code\u003e döndürür.\u003c/p\u003e\n\u003cp\u003eOrigin'i yalnızca tekrarlanabilir \u003ccode\u003e--verifier-origin detector-id=https://host\u003c/code\u003e komut satırı bayrağıyla verin. Bu yönlendirme girdisi proje yapılandırma dosyaları ve ortam değişkenlerinde özellikle yok sayılır; dolayısıyla taranan içerik kimlik bilgisinin hedefini değiştiremez. Eski \u003ccode\u003e--grafana-instance-url\u003c/code\u003e bayrağı yalnızca Grafana için alias olarak kalır.\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eDedektör ID\u003c/th\u003e\n\u003cth\u003eGerekli bağlam\u003c/th\u003e\n\u003cth\u003eÜretim davranışı\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eauth0-management-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOperatörce güvenilen Auth0 tenant veya özel alan adı origin'i\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e--verifier-origin auth0-management-token=https://tenant\u003c/code\u003e ile yapılandırılır. Dedektör tam üç parçalı Management JWT üretir; doğrulanmamış iddialar ve repo URL'leri hedef seçmez. Salt-okunur clients yoklamasında yalnız \u003ccode\u003e401\u003c/code\u003e inaktiftir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egitlab-pat\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOperatörce güvenilen GitLab.com veya self-managed API origin'i\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e--verifier-origin gitlab-pat=https://gitlab.example\u003c/code\u003e ile yapılandırılır. Repo içeriği ve finding metadata'sı hedef seçmez. Güvenli \u003ccode\u003e/api/v4/user\u003c/code\u003e yoklamasını yalnız \u003ccode\u003eglpat-\u003c/code\u003e PAT kullanır; tanınan diğer GitLab token alt türleri \u003ccode\u003eunverified\u003c/code\u003e kalır. Aktif kimlik kanıtından sonra best-effort \u003ccode\u003e/personal_access_tokens/self\u003c/code\u003e çağrısı sınırlı, doğrulanmış ve sıralanmış standart veya GitLab 19.2+ ayrıntılı kapsamları ve ISO sona erme tarihini \u003ccode\u003everification.extra_data\u003c/code\u003e alanına ekler; erişilemeyen ya da hatalı meta veri aktif kanıtı silmez. \u003ccode\u003e401\u003c/code\u003e, yalnızca katı JSON gövdesi GitLab'ın standart geçersiz-token yanıtıysa inaktiftir; DPoP challenge'ları \u003ccode\u003everify_error\u003c/code\u003e kalır.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egrafana-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e--grafana-instance-url\u003c/code\u003e ile verilen güvenilir Grafana instance origin'i\u003c/td\u003e\n\u003ctd\u003eYalnızca doğrulanmış HTTPS instance'ını çağırır. Repo içeriği veya finding metadata hedef seçemez; \u003ccode\u003e401\u003c/code\u003e yalnızca bu güvenilir issuer üzerinde inaktiftir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003etwilio-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eEşleşen API Key SID ve operatörce güvenilen bölgesel API origin'i (US1/IE1/AU1)\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e--verifier-origin twilio-api-key=https://api.twilio.com\u003c/code\u003e (veya doğru bölgesel origin) ile yapılandırılır. Tek başına \u003ccode\u003eSK...\u003c/code\u003e SID bulgu değildir; opaque secret yalnızca yakındaki atanmış SID ile bire bir eşleşirse üretilir. Yalnız \u003ccode\u003e401\u003c/code\u003e inaktiftir, izin \u003ccode\u003e403\u003c/code\u003e yanıtı \u003ccode\u003everify_error\u003c/code\u003e kalır.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eshopify-access-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eOperatörce güvenilen issuer mağaza origin'i\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e--verifier-origin shopify-access-token=https://store.myshopify.com\u003c/code\u003e ile yapılandırılır. Finding metadata'sı hedef seçmez. Verifier sabitlenmiş 2026-07 Admin GraphQL mağaza kimliği sorgusunu kullanır; yalnız seçilen mağazadaki \u003ccode\u003e401\u003c/code\u003e inaktiftir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egithub-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGüvenilir GitHub.com veya GitHub Enterprise Server API origin'i\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e--verifier-origin github-token=https://api.github.com\u003c/code\u003e veya GHES origin'iyle yapılandırılır. GHES aynı \u003ccode\u003eghp_\u003c/code\u003e ve \u003ccode\u003egithub_pat_\u003c/code\u003e biçimlerini kullanır; repo metadata'sı issuer seçemez. Issuer sağladığında doğrulanmış kapsam/sayı ve sona erme yanıt başlıkları \u003ccode\u003everification.extra_data\u003c/code\u003e alanına eklenir.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egithub-oauth-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGüvenilir GitHub.com veya GitHub Enterprise Server API origin'i\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e--verifier-origin github-oauth-token=https://api.github.com\u003c/code\u003e veya GHES origin'iyle yapılandırılır. \u003ccode\u003egho_\u003c/code\u003e/\u003ccode\u003eghu_\u003c/code\u003e, \u003ccode\u003e/user\u003c/code\u003e kullanır ve aynı güvenli kapsam/sona erme zenginleştirmesini alır; \u003ccode\u003eghs_\u003c/code\u003e installation repositories endpoint'ini kullanır. Döndürücü exchange nedeniyle \u003ccode\u003eghr_\u003c/code\u003e istek yapılmadan \u003ccode\u003eunverified\u003c/code\u003e kalır.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edatadog-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGüvenilir Datadog site/API origin'i\u003c/td\u003e\n\u003ctd\u003eTam resmî site \u003ccode\u003e--verifier-origin datadog-api-key=https://api.datadoghq.com\u003c/code\u003e (veya bölgesel/FED hostu) ile seçilir. Keyfî hostlar reddedilir; yanlış bölgenin reddi iptal kanıtı sayılmaz.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esnyk-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGüvenilir Snyk bölgesel, kamu veya özel API origin'i\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003e--verifier-origin snyk-api-key=https://api.snyk.io\u003c/code\u003e (veya doğru bölgesel/özel origin) ile yapılandırılır. Yanlış bölgenin reddi iptal kanıtı değildir. Yalnız \u003ccode\u003e401\u003c/code\u003e inaktiftir; plan/izin eksikliğini gösterebilen \u003ccode\u003e403\u003c/code\u003e, \u003ccode\u003everify_error\u003c/code\u003e kalır.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"yalnzca-format-dorulamas-6-dedektr-tr\"\u003eYalnızca format doğrulaması (6 dedektör türü)\u003c/h2\u003e\n\u003cp\u003eBu doğrulayıcılar tamamen çevrimdışı çalışır. Hiçbir ağ isteği yapılmaz. Geçerli bir format kimlik bilgisinin aktif olduğunu kanıtlamadığından, altısı da format kontrolünün geçip geçmediğinden bağımsız olarak her zaman \u003ccode\u003eunverified\u003c/code\u003e döndürür.\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eDedektör ID\u003c/th\u003e\n\u003cth\u003eDoğrulanan özellik\u003c/th\u003e\n\u003cth\u003eNeden canlı kontrol yok\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egcp-service-account\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eJSON yapısı (\u003ccode\u003etype\u003c/code\u003e, \u003ccode\u003eproject_id\u003c/code\u003e, \u003ccode\u003eprivate_key_id\u003c/code\u003e, \u003ccode\u003eclient_email\u003c/code\u003e)\u003c/td\u003e\n\u003ctd\u003eCanlı kontrol, yan etkileri olan GCP OAuth2 token değişimi gerektirir\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003erabbitmq-connection-string\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eAMQP URL'nin başarıyla ayrıştırılması\u003c/td\u003e\n\u003ctd\u003eHerkese açık kimlik doğrulamasız sağlık uç noktası yok\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003esnowflake-credentials\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eParola uzunluğu ve host alt dize kontrolü\u003c/td\u003e\n\u003ctd\u003eCanlı kontrol bir JDBC/ODBC veritabanı bağlantısı gerektirir\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eazure-storage-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFormat kontrolü\u003c/td\u003e\n\u003ctd\u003eHesap başına HMAC imzalama gerektirir; genel kimlik uç noktası yok\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eazure-entra-secret\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFormat kontrolü\u003c/td\u003e\n\u003ctd\u003eİstemci kimlik bilgisi akışı oturum oluşturur\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ecoinbase-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eKarakter kümesi ve uzunluk kontrolü\u003c/td\u003e\n\u003ctd\u003eCoinbase'in eski API'si, dedektörün anahtarla güvenilir biçimde ilişkilendiremeyeceği eşleştirilmiş sırrı gerektiren HMAC-SHA256 istek imzalamayla kimlik doğrular; canlı doğrulama denenmez, böylece gerçek bir anahtar hiçbir zaman yanlışlıkla inaktif olarak raporlanmaz\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"dorulanamaz-11-dedektr-tr\"\u003eDoğrulanamaz (11 dedektör türü)\u003c/h2\u003e\n\u003cp\u003eBu dedektör türlerinin hiç doğrulayıcısı yoktur. Bunlardan gelen bulgular her zaman \u003ccode\u003eunverified\u003c/code\u003e olur. Bu durum önemsiz oldukları anlamına \u003cstrong\u003egelmez\u003c/strong\u003e — tam olarak tespit edilip raporlanırlar — ancak herkese açık bir doğrulama API'si bulunmamakta ya da herhangi bir doğrulama girişimi yan etkiye yol açmaktadır.\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eDedektör ID\u003c/th\u003e\n\u003cth\u003eNeden\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ejwt\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eJWT herhangi bir tarafça yayınlanabilir; evrensel bir doğrulama uç noktası yoktur\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eprivate-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eÇağrılacak sağlayıcı yok; aktif kullanım uzaktan tespit edilemez\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003egeneric-api-key\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eTanım gereği bilinmeyen sağlayıcı\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003edatabase-connection-string\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBağlanmak hedef veritabanında oturum oluşturur\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eredis-connection-string\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBağlanmak Redis örneğinde canlı bağlantı açar\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eftp-credentials\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eGüvenli, salt-okunur FTP yoklama yöntemi yok\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eldap-credentials\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eLDAP bind kimliği doğrulanmış bir oturum oluşturur\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003eslack-webhook\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eWebhook'un aktif olduğunu doğrulamak mesaj göndermeyi gerektirir\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ehashicorp-vault-token\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eVault token doğrulaması, Vault uç noktasının bilinmesini gerektirir\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003ediscord-webhook-url\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBir webhook'un aktif olduğunu doğrulamak, ona bir mesaj göndermeyi gerektirir\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ccode\u003estructured-config-secret\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eBağlamsal fallback bir secret rolünü belirleyebilir ancak credential sağlayıcısını veya issuer'ı belirleyemez\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cdiv class=\"callout callout-note\"\u003e\u003cdiv class=\"callout-label\"\u003eNot\u003c/div\u003e\u003cdiv class=\"callout-body\"\u003e\u003cp\u003e\u0026quot;Doğrulanamaz\u0026quot; \u0026quot;bulunamaz\u0026quot; anlamına gelmez. Bu 11 türün tamamı yine de tespit edilir ve çıktınızda görünür. Kimlik bilgisinin canlı olup olmadığını ve döndürülmesi gerekip gerekmediğini belirlemek için manuel inceleme gerektirir.\u003c/p\u003e\n\u003c/div\u003e\u003c/div\u003e\n\u003ch2 id=\"kapsam-zeti\"\u003eKapsam özeti\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eKategori\u003c/th\u003e\n\u003cth\u003eSayı\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003eCanlı doğrulanan\u003c/td\u003e\n\u003ctd\u003e39\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eGüvenilir/eşlik eden bağlam gerektiren\u003c/td\u003e\n\u003ctd\u003e9\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eYalnızca format doğrulaması\u003c/td\u003e\n\u003ctd\u003e6\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eDoğrulanamaz\u003c/td\u003e\n\u003ctd\u003e11\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eToplam dedektör\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003e\u003cstrong\u003e65\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cstrong\u003eKayıtlı doğrulayıcı implementasyonu\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003e\u003cstrong\u003e54 (%83,1)\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"ayrca-bakn\"\u003eAyrıca bakın\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#/verification/how-verification-works\"\u003eDoğrulama Nasıl Çalışır\u003c/a\u003e — iki doğrulama modu, durumlar ve doğrulama motoru.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#/detectors/detector-catalog\"\u003eDedektör Kataloğu\u003c/a\u003e — yerleşik dedektörlerin tam listesi ve önem dereceleri.\u003c/li\u003e\n\u003c/ul\u003e\n"}}; diff --git a/site/js/scanner.js b/site/js/scanner.js index 4d962f6..bda8132 100644 --- a/site/js/scanner.js +++ b/site/js/scanner.js @@ -13,15 +13,15 @@ var detCountEl = document.getElementById("detCount"); var MIN_MATCH = 8; // ignore matches shorter than this (kills context-gate noise) - var INPUT_CAP = 64 * 1024; // guard against pathological input + var INPUT_CAP = 64 * 1024; // UTF-8 byte cap guarding pathological input function t(k, fb) { return (window.LWI18n && window.LWI18n.t(k)) || fb || k; } // Compile the usable detector set from the build-extracted patterns. - var DETS = (window.LW_DETECTORS || []).map(function (d) { - var res = (d.patterns || []).map(function (p) { + function compilePatterns(patterns) { + return (patterns || []).map(function (p) { try { var flags = "g" + (p.flags || "").replace(/g/g, ""); return new RegExp(p.src, flags); @@ -29,7 +29,21 @@ return null; } }).filter(Boolean); - return { id: d.id, sev: d.severity, kw: d.keywords || [], res: res }; + } + + var DETS = (window.LW_DETECTORS || []).map(function (d) { + var correlation = d.correlation || {}; + return { + id: d.id, + sev: d.severity, + kw: d.keywords || [], + res: compilePatterns(d.patterns), + required: compilePatterns(correlation.requiredNearby), + proximity: correlation.proximityBytes || 0, + sameBlock: correlation.sameLogicalBlock === true, + rejectPlaceholders: correlation.rejectPlaceholders === true, + oneToOne: correlation.oneToOne === true, + }; }).filter(function (d) { return d.res.length; }); if (detCountEl) detCountEl.textContent = String(DETS.length); @@ -78,44 +92,198 @@ }); } + function utf8Width(codePoint) { + if (codePoint <= 0x7f) return 1; + if (codePoint <= 0x7ff) return 2; + if (codePoint <= 0xffff) return 3; + return 4; + } + + function truncateUTF8(value, byteCap) { + var bytes = 0, index = 0; + while (index < value.length) { + var codePoint = value.codePointAt(index); + var units = codePoint > 0xffff ? 2 : 1; + var width = utf8Width(codePoint); + if (bytes + width > byteCap) break; + bytes += width; + index += units; + } + return { text: value.slice(0, index), truncated: index < value.length }; + } + + function utf8PrefixOffsets(value) { + var offsets = new Uint32Array(value.length + 1); + var bytes = 0, index = 0; + while (index < value.length) { + offsets[index] = bytes; + var codePoint = value.codePointAt(index); + var units = codePoint > 0xffff ? 2 : 1; + if (units === 2) offsets[index + 1] = bytes; + bytes += utf8Width(codePoint); + index += units; + offsets[index] = bytes; + } + return offsets; + } + + function rangeGap(byteOffsets, a, b) { + if (a.end <= b.start) return byteOffsets[b.start] - byteOffsets[a.end]; + if (b.end <= a.start) return byteOffsets[a.start] - byteOffsets[b.end]; + return 0; + } + + function sameLogicalBlock(text, a, b) { + var start = a.end, end = b.start; + if (b.end <= a.start) { start = b.end; end = a.start; } + if (start > end) return false; + var between = text.slice(start, end); + return !/(?:\r?\n[ \t]*\r?\n|\r?\n[ \t]*\[[^\r\n]+\][ \t]*(?:\r?\n|$))/.test(between) && + !/[{}]/.test(between); + } + + function correlatedAssignmentStart(match) { + var whole = match[0]; + if (!whole || /^[0-9A-Za-z]/.test(whole)) return match.index; + return match.index + (whole.codePointAt(0) > 0xffff ? 2 : 1); + } + + function allMatchRanges(regexes, text) { + var ranges = []; + regexes.forEach(function (re) { + re.lastIndex = 0; + var match; + while ((match = re.exec(text)) !== null) { + var whole = match[0], captured = ""; + for (var i = 1; i < match.length; i++) { + if (match[i] && match[i].length > captured.length) captured = match[i]; + } + var end = re.lastIndex; + if (captured) end = match.index + whole.lastIndexOf(captured) + captured.length; + ranges.push({ start: correlatedAssignmentStart(match), end: end, used: false }); + if (match.index === re.lastIndex) re.lastIndex++; + } + }); + return ranges; + } + + function isPlaceholder(value) { + var lower = String(value || "").trim().toLowerCase(); + var exact = { + "change-me": true, "change_me": true, "changeme": true, "dummy": true, + "example": true, "example-secret": true, "example_secret": true, "fixme": true, + "foobar": true, "not-a-real-secret": true, "not_a_real_secret": true, + "placeholder": true, "redacted": true, "replace-me": true, "replace_me": true, + "secret": true, "string": true, "test": true, "todo": true, + "api-key-secret": true, "api_key_secret": true, + "twilio-api-key-secret": true, "twilio_api_key_secret": true, "twilioapikeysecret": true, + "your-api-key-secret": true, "your_api_key_secret": true, + "your-twilio-api-key-secret": true, "your_twilio_api_key_secret": true, "xxxxxxxx": true, + }; + if (!lower || exact[lower]) return true; + var prefixes = ["${", "$", "{{", "%", "<", "vault://", "op://", "secret://", "file://", "/run/secrets/", "@microsoft.keyvault"]; + if (prefixes.some(function (prefix) { return lower.indexOf(prefix) === 0; })) return true; + return lower.length >= 4 && /^[x*0-]+$/.test(lower); + } + + function escapedAt(value, index) { + var backslashes = 0; + for (var i = index - 1; i >= 0 && value[i] === "\\"; i--) backslashes++; + return backslashes % 2 === 1; + } + var lastFound = [], lastTruncated = false; - function scan() { - var text = codeEl.value; - var truncated = false; - if (text.length > INPUT_CAP) { text = text.slice(0, INPUT_CAP); truncated = true; } + function capturedDisplay(match, detector) { + var captured = ""; + for (var i = 1; i < match.length; i++) { + if (match[i] && match[i].length > captured.length && (detector.required.length || match[i].length >= MIN_MATCH)) { + captured = match[i]; + } + } + return captured || match[0]; + } + + function closestCompanionIndex(detector, text, byteOffsets, primaryRange, companions) { + var companionIndex = -1, bestDistance = -1; + companions.forEach(function (companion, index) { + if (detector.oneToOne && companion.used) return; + var distance = rangeGap(byteOffsets, primaryRange, companion); + if (distance > detector.proximity || (detector.sameBlock && !sameLogicalBlock(text, primaryRange, companion))) return; + if (bestDistance === -1 || distance < bestDistance) { + bestDistance = distance; + companionIndex = index; + } else if (distance === bestDistance) { + var current = companionIndex >= 0 ? companions[companionIndex] : null; + var candidatePrecedes = companion.end <= primaryRange.start; + var currentPrecedes = current && current.end <= primaryRange.start; + if (candidatePrecedes && !currentPrecedes) companionIndex = index; + } + }); + return companionIndex; + } + + function recordDetectorMatch(detector, text, byteOffsets, companions, match, found, seen) { + var whole = match[0]; + var display = capturedDisplay(match, detector); + var valueOffset = whole.lastIndexOf(display); + var primaryRange = { start: correlatedAssignmentStart(match), end: match.index + valueOffset + display.length }; + var closing = valueOffset + display.length; + var truncatedQuotedValue = closing < whole.length && (whole[closing] === '"' || whole[closing] === "'") && escapedAt(whole, closing); + var companionIndex = closestCompanionIndex(detector, text, byteOffsets, primaryRange, companions); + var correlated = !detector.required.length || companionIndex >= 0; + if (!correlated || truncatedQuotedValue || whole.length < MIN_MATCH || (detector.rejectPlaceholders && isPlaceholder(display))) return; + if (detector.oneToOne && companionIndex >= 0) companions[companionIndex].used = true; + var line = lineOf(text, match.index); + var key = detector.id + "|" + display + "|" + line; + if (!seen[key]) { + seen[key] = true; + found.push({ id: detector.id, sev: detector.sev, line: line, val: display }); + } + } + + function scanDetectorPattern(detector, re, text, byteOffsets, companions, found, seen) { + re.lastIndex = 0; + var match; + while ((match = re.exec(text)) !== null) { + recordDetectorMatch(detector, text, byteOffsets, companions, match, found, seen); + if (match.index === re.lastIndex) re.lastIndex++; // avoid zero-width loop + } + } + + function detectText(input) { + var bounded = truncateUTF8(String(input || ""), INPUT_CAP); + var text = bounded.text; + var truncated = bounded.truncated; + var byteOffsets = utf8PrefixOffsets(text); var lower = text.toLowerCase(); var found = [], seen = {}; DETS.forEach(function (d) { // Aho-Corasick-style keyword pre-filter (faithful to the engine). if (d.kw.length && !d.kw.some(function (k) { return lower.indexOf(k.toLowerCase()) !== -1; })) return; + var companions = allMatchRanges(d.required, text); d.res.forEach(function (re) { - re.lastIndex = 0; - var m; - while ((m = re.exec(text)) !== null) { - var whole = m[0]; - if (whole.length >= MIN_MATCH) { - // Prefer the longest capture group (the secret) for display. - var disp = whole; - for (var i = 1; i < m.length; i++) { - if (m[i] && m[i].length >= MIN_MATCH && m[i].length < disp.length) disp = m[i]; - } - var line = lineOf(text, m.index); - var key = d.id + "|" + disp + "|" + line; - if (!seen[key]) { seen[key] = true; found.push({ id: d.id, sev: d.sev, line: line, val: disp }); } - } - if (m.index === re.lastIndex) re.lastIndex++; // avoid zero-width loop - } + scanDetectorPattern(d, re, text, byteOffsets, companions, found, seen); }); }); var order = { critical: 0, high: 1, medium: 2, low: 3 }; found.sort(function (a, b) { return (order[a.sev] - order[b.sev]) || (a.line - b.line); }); - lastFound = found; lastTruncated = truncated; - render(found, truncated); + return { found: found, truncated: truncated }; } + function scan() { + var result = detectText(codeEl.value); + lastFound = result.found; lastTruncated = result.truncated; + render(result.found, result.truncated); + } + + // A side-effect-free hook used by the generated-detector contract test. It + // also makes the playground's preview semantics independently auditable. + window.LW_PLAYGROUND_DETECT = function (text) { return detectText(text).found; }; + window.LW_PLAYGROUND_SCAN = detectText; + function render(found, truncated) { countEl.textContent = found.length ? found.length + " " + t("play.count.detected", "detected") diff --git a/site/js/translations.js b/site/js/translations.js index d1d04c6..3c3f95f 100644 --- a/site/js/translations.js +++ b/site/js/translations.js @@ -28,12 +28,12 @@ window.LW_T = { "hero.lede": "Detect, verify, and report leaked API keys, tokens, and credentials across code, Git history, containers, and the cloud — then watch the redactions lift.", "hero.cta.run": "▸ Run the scan", "hero.cta.docs": "Read the docs", - "hero.kv": "64 detectors · 54 live verifiers · 6 sources · exit-code aware", + "hero.kv": "65 detectors · 39 direct-live · 9 context-required · 6 sources", "hero.doc.redacted": "3 REDACTED", "hero.doc.hint": "Leakwatch finds the secrets that shouldn't be visible.", "stats.detectors": "secret detectors", - "stats.verifiers": "live verifiers", + "stats.verifiers": "direct live checks", "stats.sources": "scan sources", "stats.formats": "output formats", @@ -75,23 +75,26 @@ window.LW_T = { "chain.s5.d": "Filter by severity; emit JSON, SARIF, CSV, or a table.", "verify.title": "Is it still live?", - "verify.intro": "Detection is half the job. For most secret types, Leakwatch makes a controlled, read-only API call to the provider to confirm whether a key is active — separating real incidents from noise.", - "verify.live.k": "Live verified", - "verify.live.d": "A read-only API call confirms the key is active or inactive.", + "verify.intro": "Detection is half the job. Leakwatch distinguishes direct live checks from checks that require trusted context and from offline format validation, so registry counts never overstate live capability.", + "verify.live.k": "Direct live", + "verify.live.d": "A non-destructive provider check can confirm the key in the normal production path.", + "verify.context.k": "Context required", + "verify.context.d": "A trusted issuer, region, or paired credential must be supplied before a safe live check.", "verify.fmt.k": "Format checked", "verify.fmt.d": "Validated by structure where no safe live check exists.", "verify.no.k": "Not verifiable", - "verify.no.d": "No public verification API (e.g. JWTs, private keys) — still detected, triaged manually.", + "verify.no.d": "No safe provider check (e.g. JWTs, private keys) — still detected, triaged manually.", "formats.title": "Reports that fit your workflow", "formats.json": "Structured findings for tooling and automation.", "formats.sarif": "v2.1.0 — upload straight to GitHub Code Scanning.", "formats.csv": "Spreadsheet-ready, sanitized against formula injection.", "formats.table": "Colorized terminal output for local triage.", + "formats.github": "Inline pull-request annotations in GitHub Actions.", - "index.title": "Redacted index — 64 detectors", - "index.intro": "A sample of the catalog. 54 of these can be verified against the live provider. Add your own with YAML custom rules.", - "index.more": "+ 44 more · view full catalog →", + "index.title": "Redacted index — 65 detectors", + "index.intro": "A sample of the catalog: 39 direct-live, 9 context-required, 6 format-only, and 11 without a verifier. Add your own with YAML custom rules.", + "index.more": "+ 46 more · view full catalog →", "cta.title": "Declassify your repo", "cta.subtitle": "One static binary. No daemon, no runtime dependencies. Install it your way.", @@ -154,7 +157,7 @@ window.LW_T = { "play.eyebrow": "▸ Try it", "play.title": "Scan it yourself.", "play.lede": "Paste a config file, an .env, a diff — anything. Leakwatch's real detection patterns run right here in your browser. No upload, no server.", - "play.detsuffix": "of 64 detectors run in your browser · your input is never sent anywhere", + "play.detsuffix": "of 65 built-in detectors run in your browser · your input is never sent anywhere", "play.editor.file": "paste-here.txt", "play.sample.env": ".env", "play.sample.code": "source", @@ -201,12 +204,12 @@ window.LW_T = { "hero.lede": "Sızan API anahtarlarını, token'ları ve kimlik bilgilerini kodda, Git geçmişinde, konteynerlerde ve bulutta tespit edin, doğrulayın ve raporlayın — sonra sansürlerin kalkışını izleyin.", "hero.cta.run": "▸ Taramayı çalıştır", "hero.cta.docs": "Dokümanları oku", - "hero.kv": "63 dedektör · 54 canlı doğrulayıcı · 6 kaynak · çıkış-kodu duyarlı", + "hero.kv": "65 dedektör · 39 doğrudan canlı · 9 bağlam gerektiren · 6 kaynak", "hero.doc.redacted": "3 REDAKTE", "hero.doc.hint": "Leakwatch, görünür olmaması gereken sırları bulur.", "stats.detectors": "sır dedektörü", - "stats.verifiers": "canlı doğrulayıcı", + "stats.verifiers": "doğrudan canlı kontrol", "stats.sources": "tarama kaynağı", "stats.formats": "çıktı formatı", @@ -248,23 +251,26 @@ window.LW_T = { "chain.s5.d": "Önem derecesine göre filtreler; JSON, SARIF, CSV veya tablo üretir.", "verify.title": "Hâlâ etkin mi?", - "verify.intro": "Tespit işin yarısıdır. Çoğu sır türü için Leakwatch, bir anahtarın etkin olup olmadığını teyit etmek üzere sağlayıcıya kontrollü, salt-okunur bir API çağrısı yapar — gerçek olayları gürültüden ayırır.", - "verify.live.k": "Canlı doğrulanan", - "verify.live.d": "Salt-okunur bir API çağrısı anahtarın etkin veya etkisiz olduğunu teyit eder.", + "verify.intro": "Tespit işin yarısıdır. Leakwatch doğrudan canlı kontrolleri, güvenilir bağlam gerektiren kontrollerden ve çevrimdışı format doğrulamasından ayırır; böylece registry sayısı canlı kabiliyeti abartmaz.", + "verify.live.k": "Doğrudan canlı", + "verify.live.d": "Yıkıcı olmayan bir sağlayıcı kontrolü anahtarı normal üretim yolunda doğrulayabilir.", + "verify.context.k": "Bağlam gerekli", + "verify.context.d": "Güvenli canlı kontrolden önce güvenilir issuer, bölge veya eşleştirilmiş kimlik bilgisi sağlanmalıdır.", "verify.fmt.k": "Format kontrollü", "verify.fmt.d": "Güvenli bir canlı kontrolün olmadığı yerlerde yapısına göre doğrulanır.", "verify.no.k": "Doğrulanamayan", - "verify.no.d": "Genel bir doğrulama API'si yok (örn. JWT'ler, özel anahtarlar) — yine de tespit edilir, elle triyaj yapılır.", + "verify.no.d": "Güvenli sağlayıcı kontrolü yok (örn. JWT'ler, özel anahtarlar) — yine de tespit edilir, elle triyaj yapılır.", "formats.title": "İş akışınıza uyan raporlar", "formats.json": "Araçlar ve otomasyon için yapılandırılmış bulgular.", "formats.sarif": "v2.1.0 — doğrudan GitHub Code Scanning'e yükleyin.", "formats.csv": "Hesap tablosuna hazır, formül enjeksiyonuna karşı temizlenmiş.", "formats.table": "Yerel triyaj için renklendirilmiş terminal çıktısı.", + "formats.github": "GitHub Actions içinde satır içi pull-request ek açıklamaları.", - "index.title": "Redakteli indeks — 63 dedektör", - "index.intro": "Kataloğun bir örneği. Bunların 54'ü canlı sağlayıcıya karşı doğrulanabilir. YAML özel kurallarıyla kendi dedektörlerinizi ekleyin.", - "index.more": "+ 44 tane daha · tüm kataloğu görüntüle →", + "index.title": "Redakteli indeks — 65 dedektör", + "index.intro": "Katalogdan bir örnek: 39 doğrudan canlı, 9 bağlam gerektiren, 6 yalnız format ve 11 doğrulayıcısız. YAML özel kurallarıyla kendi dedektörlerinizi ekleyin.", + "index.more": "+ 46 tane daha · tüm kataloğu görüntüle →", "cta.title": "Deponuzun gizliliğini kaldırın", "cta.subtitle": "Tek bir statik ikili dosya. Daemon yok, çalışma zamanı bağımlılığı yok. İstediğiniz yöntemle kurun.", @@ -327,7 +333,7 @@ window.LW_T = { "play.eyebrow": "▸ Dene", "play.title": "Kendin tara.", "play.lede": "Bir yapılandırma dosyası, bir .env, bir diff — ne olursa. Leakwatch'ın gerçek tespit desenleri tam burada, tarayıcında çalışır. Yükleme yok, sunucu yok.", - "play.detsuffix": "/ 63 dedektör tarayıcında çalışır · girdiniz hiçbir yere gönderilmez", + "play.detsuffix": "/ 65 yerleşik dedektör tarayıcında çalışır · girdiniz hiçbir yere gönderilmez", "play.editor.file": "buraya-yapıştır.txt", "play.sample.env": ".env", "play.sample.code": "kaynak", diff --git a/site/playground.html b/site/playground.html index afe3329..0a2ce3e 100644 --- a/site/playground.html +++ b/site/playground.html @@ -69,7 +69,7 @@
    ▸ Try it

    Scan it yourself.

    Paste a config file, an .env, a diff — anything. Leakwatch's real detection patterns run right here in your browser. No upload, no server.

    -
    🔒 — of 64 detectors run in your browser · your input is never sent anywhere
    +
    🔒 — of 65 built-in detectors run in your browser · your input is never sent anywhere
    @@ -130,7 +130,7 @@

    License (MIT) - + diff --git a/tools/site-build/detectors.go b/tools/site-build/detectors.go index f5e4fca..87da201 100644 --- a/tools/site-build/detectors.go +++ b/tools/site-build/detectors.go @@ -55,11 +55,20 @@ type detPattern struct { Flags string `json:"flags,omitempty"` } +type detCorrelation struct { + RequiredNearby []detPattern `json:"requiredNearby"` + ProximityBytes int `json:"proximityBytes"` + SameLogicalBlock bool `json:"sameLogicalBlock,omitempty"` + RejectPlaceholders bool `json:"rejectPlaceholders,omitempty"` + OneToOne bool `json:"oneToOne,omitempty"` +} + type detEntry struct { - ID string `json:"id"` - Severity string `json:"severity"` - Keywords []string `json:"keywords"` - Patterns []detPattern `json:"patterns"` + ID string `json:"id"` + Severity string `json:"severity"` + Keywords []string `json:"keywords"` + Patterns []detPattern `json:"patterns"` + Correlation *detCorrelation `json:"correlation,omitempty"` } // Packages whose detection depends on logic the browser can't fa­ithfully @@ -139,7 +148,7 @@ func buildDetectors(root, jsDir string, strict bool) (int, error) { sort.Strings(dropped) for _, id := range dropped { - fmt.Fprintf(os.Stderr, "site-build: WARNING detector %q discovered but emitted zero playground patterns (all references were gates, extraction-only, or conditionally gated)\n", id) + fmt.Fprintf(os.Stderr, "site-build: WARNING detector %q discovered but emitted zero playground patterns (references were gated/extraction-only, or its correlated playground contract was not fully representable)\n", id) } if len(dropped) > 0 && strict { return 0, fmt.Errorf("%d detector(s) emitted zero playground patterns (strict mode): %s", len(dropped), strings.Join(dropped, ", ")) @@ -160,13 +169,34 @@ func buildDetectors(root, jsDir string, strict bool) (int, error) { // extractDetectors returns the emitted entries plus the IDs of any detector // types discovered in f that ended up with zero safe, unconditional patterns. func extractDetectors(f *ast.File) (out []detEntry, dropped []string) { - // Named regexp vars (name -> raw pattern) and every MustCompile literal in + // Named regexp vars (name -> raw pattern), integer constants, and every MustCompile literal in // the file (covers patterns declared inside slices, used as a fallback). varPat := map[string]string{} + intConst := map[string]int{} var filePats []string for _, decl := range f.Decls { gd, ok := decl.(*ast.GenDecl) - if !ok || gd.Tok != token.VAR { + if !ok { + continue + } + if gd.Tok == token.CONST { + for _, spec := range gd.Specs { + vs, ok := spec.(*ast.ValueSpec) + if !ok { + continue + } + for i, value := range vs.Values { + if i >= len(vs.Names) { + continue + } + if n, ok := integerLiteral(value); ok { + intConst[vs.Names[i].Name] = n + } + } + } + continue + } + if gd.Tok != token.VAR { continue } for _, spec := range gd.Specs { @@ -191,7 +221,7 @@ func extractDetectors(f *ast.File) (out []detEntry, dropped []string) { }) // Group the detector methods by receiver type. - type methods struct{ id, kw, sev, scan *ast.FuncDecl } + type methods struct{ id, kw, sev, scan, playground *ast.FuncDecl } types := map[string]*methods{} getType := func(name string) *methods { if types[name] == nil { @@ -217,6 +247,8 @@ func extractDetectors(f *ast.File) (out []detEntry, dropped []string) { getType(rt).sev = fd case "Scan": getType(rt).scan = fd + case "PlaygroundPatternContract": + getType(rt).playground = fd } } @@ -233,8 +265,15 @@ func extractDetectors(f *ast.File) (out []detEntry, dropped []string) { entry.Keywords = []string{} } - var pats []string - if m.scan != nil { + var pats, requiredNearby []string + correlationOK := true + proximityBytes := 0 + sameLogicalBlock := false + rejectPlaceholders := false + oneToOne := false + if m.playground != nil { + pats, requiredNearby, proximityBytes, sameLogicalBlock, rejectPlaceholders, oneToOne, correlationOK = playgroundCorrelationOf(m.playground, varPat, intConst) + } else if m.scan != nil { refs := newPatternRefSet() collectPatternRefs(m.scan.Body.List, false, refs) if len(refs.order) == 0 { @@ -258,14 +297,58 @@ func extractDetectors(f *ast.File) (out []detEntry, dropped []string) { } seen := map[string]bool{} - for _, p := range dedup(pats) { + patternsToEmit := dedup(pats) + if m.playground != nil { + patternsToEmit = pats // correlated contracts are already strictly unique + } + for _, p := range patternsToEmit { src, flags, ok := goToJSRegex(p) - if !ok || seen[src+"\x00"+flags] { + if !ok { + if m.playground != nil { + correlationOK = false + break + } + continue + } + if seen[src+"\x00"+flags] { + if m.playground != nil { + correlationOK = false + break + } continue } seen[src+"\x00"+flags] = true entry.Patterns = append(entry.Patterns, detPattern{Src: src, Flags: flags}) } + if m.playground != nil && correlationOK { + correlation := &detCorrelation{ + ProximityBytes: proximityBytes, + SameLogicalBlock: sameLogicalBlock, + RejectPlaceholders: rejectPlaceholders, + OneToOne: oneToOne, + } + requiredSeen := map[string]bool{} + for _, p := range dedup(requiredNearby) { + src, flags, ok := goToJSRegex(p) + key := src + "\x00" + flags + if !ok || requiredSeen[key] { + correlationOK = false + break + } + requiredSeen[key] = true + correlation.RequiredNearby = append(correlation.RequiredNearby, detPattern{Src: src, Flags: flags}) + } + if len(correlation.RequiredNearby) == 0 || proximityBytes <= 0 { + correlationOK = false + } + if correlationOK { + entry.Correlation = correlation + } + } + if !correlationOK { + entry.Patterns = nil + entry.Correlation = nil + } if len(entry.Patterns) > 0 { out = append(out, entry) } else { @@ -275,6 +358,174 @@ func extractDetectors(f *ast.File) (out []detEntry, dropped []string) { return out, dropped } +// playgroundCorrelationOf resolves the optional, co-located detector contract +// without importing the parent module. It deliberately accepts only a direct +// detector.PlaygroundPatternContract composite literal whose regex fields +// reference package-level regexp vars and whose proximity is a positive +// integer literal or constant. Unsupported shapes fail closed in strict mode. +func playgroundCorrelationOf(fd *ast.FuncDecl, patterns map[string]string, ints map[string]int) (primary, required []string, proximity int, sameBlock, rejectPlaceholders, oneToOne, ok bool) { + contract, valid := directPlaygroundContract(fd) + if !valid { + return nil, nil, 0, false, false, false, false + } + values := playgroundCorrelationValues{} + seenFields := map[string]bool{} + for _, element := range contract.Elts { + field, fieldName, valid := playgroundContractField(element, seenFields) + if !valid || !values.applyField(fieldName, field.Value, patterns, ints) { + return nil, nil, 0, false, false, false, false + } + seenFields[fieldName] = true + } + requiredFields := seenFields["Primary"] && seenFields["RequiredNearby"] && seenFields["ProximityBytes"] + ok = requiredFields && len(values.primary) > 0 && len(values.required) > 0 && values.proximity > 0 + return values.primary, values.required, values.proximity, values.sameBlock, values.rejectPlaceholders, values.oneToOne, ok +} + +type playgroundCorrelationValues struct { + primary, required []string + proximity int + sameBlock, rejectPlaceholders bool + oneToOne bool +} + +func directPlaygroundContract(fd *ast.FuncDecl) (*ast.CompositeLit, bool) { + if fd == nil || fd.Body == nil || len(fd.Body.List) != 1 { + return nil, false + } + ret, isReturn := fd.Body.List[0].(*ast.ReturnStmt) + if !isReturn || len(ret.Results) != 1 { + return nil, false + } + contract, isContract := ret.Results[0].(*ast.CompositeLit) + if !isContract || !isPlaygroundContractType(contract.Type) { + return nil, false + } + return contract, true +} + +func playgroundContractField(element ast.Expr, seen map[string]bool) (*ast.KeyValueExpr, string, bool) { + field, isField := element.(*ast.KeyValueExpr) + if !isField { + return nil, "", false + } + name, isName := field.Key.(*ast.Ident) + if !isName || seen[name.Name] { + return nil, "", false + } + return field, name.Name, true +} + +func (values *playgroundCorrelationValues) applyField(name string, value ast.Expr, patterns map[string]string, ints map[string]int) bool { + switch name { + case "Primary": + var valid bool + values.primary, valid = regexpIdentifierList(value, patterns) + return valid + case "RequiredNearby": + var valid bool + values.required, valid = regexpIdentifierList(value, patterns) + return valid + case "ProximityBytes": + var valid bool + values.proximity, valid = integerValue(value, ints) + return valid + case "SameLogicalBlock": + var valid bool + values.sameBlock, valid = boolLiteral(value) + return valid + case "RejectPlaceholders": + var valid bool + values.rejectPlaceholders, valid = boolLiteral(value) + return valid + case "OneToOne": + var valid bool + values.oneToOne, valid = boolLiteral(value) + return valid + default: + return false + } +} + +func isPlaygroundContractType(expr ast.Expr) bool { + selector, ok := expr.(*ast.SelectorExpr) + if !ok || selector.Sel.Name != "PlaygroundPatternContract" { + return false + } + pkg, ok := selector.X.(*ast.Ident) + return ok && pkg.Name == "detector" +} + +func regexpIdentifierList(expr ast.Expr, patterns map[string]string) ([]string, bool) { + list, ok := expr.(*ast.CompositeLit) + if !ok || !isRegexpPointerSliceType(list.Type) || len(list.Elts) == 0 { + return nil, false + } + out := make([]string, 0, len(list.Elts)) + seenNames := map[string]bool{} + seenPatterns := map[string]bool{} + for _, element := range list.Elts { + ident, ok := element.(*ast.Ident) + if !ok || seenNames[ident.Name] { + return nil, false + } + pattern, exists := patterns[ident.Name] + if !exists || seenPatterns[pattern] { + return nil, false + } + seenNames[ident.Name] = true + seenPatterns[pattern] = true + out = append(out, pattern) + } + return out, true +} + +func isRegexpPointerSliceType(expr ast.Expr) bool { + array, ok := expr.(*ast.ArrayType) + if !ok || array.Len != nil { + return false + } + pointer, ok := array.Elt.(*ast.StarExpr) + if !ok { + return false + } + selector, ok := pointer.X.(*ast.SelectorExpr) + if !ok || selector.Sel.Name != "Regexp" { + return false + } + pkg, ok := selector.X.(*ast.Ident) + return ok && pkg.Name == "regexp" +} + +func boolLiteral(expr ast.Expr) (bool, bool) { + ident, ok := expr.(*ast.Ident) + if !ok || (ident.Name != "true" && ident.Name != "false") { + return false, false + } + return ident.Name == "true", true +} + +func integerLiteral(expr ast.Expr) (int, bool) { + lit, ok := expr.(*ast.BasicLit) + if !ok || lit.Kind != token.INT { + return 0, false + } + n, err := strconv.Atoi(lit.Value) + return n, err == nil +} + +func integerValue(expr ast.Expr, ints map[string]int) (int, bool) { + if n, ok := integerLiteral(expr); ok { + return n, true + } + ident, ok := expr.(*ast.Ident) + if !ok { + return 0, false + } + n, ok := ints[ident.Name] + return n, ok +} + // patternRefs accumulates how a pattern var was referenced across Scan. type patternRefs struct { trigger bool // seen as the receiver of a Find*-family call, unconditionally diff --git a/tools/site-build/detectors_test.go b/tools/site-build/detectors_test.go index 72a4f46..0d8e5a4 100644 --- a/tools/site-build/detectors_test.go +++ b/tools/site-build/detectors_test.go @@ -1,9 +1,13 @@ package main import ( + "fmt" "go/ast" "go/parser" "go/token" + "os" + "os/exec" + "path/filepath" "testing" ) @@ -104,6 +108,32 @@ func parseSnippet(t *testing.T, src string) *ast.File { return f } +func correlatedContractSnippet(body string) string { + return fmt.Sprintf(` +package example + +import ( + "regexp" + "example/internal/detector" +) + +const nearby = 128 +const unresolvedSource = "UNKNOWN" +const configured = true +var supported = regexp.MustCompile("SECRET=([^\\s]+)") +var unsupported = regexp.MustCompile("(?U)a+") +var companion = regexp.MustCompile("SID=(SK[0-9a-f]{32})") +var unresolved = regexp.MustCompile(unresolvedSource) + +type Detector struct{} +func (*Detector) ID() string { return "correlated-invalid" } +func (*Detector) Scan(data []byte) { _ = supported.FindAllSubmatchIndex(data, -1) } +func (*Detector) PlaygroundPatternContract() detector.PlaygroundPatternContract { +%s +} +`, body) +} + func TestExtractDetectors_UnconditionalSinglePattern(t *testing.T) { src := ` package example @@ -363,3 +393,218 @@ func (d *Detector) Scan(data []byte) { t.Fatalf("expected example-const-ident to be reported as dropped, got: %v", dropped) } } + +func TestExtractDetectors_CorrelatedContractPreservesANDSemantics(t *testing.T) { + src := ` +package example + +import ( + "regexp" + "example/internal/detector" +) + +const nearby = 128 +var primary = regexp.MustCompile(` + "`SECRET=([^\\s]+)`" + `) +var companion = regexp.MustCompile(` + "`SID=(SK[0-9a-f]{32})`" + `) +var metadata = regexp.MustCompile(` + "`ACCOUNT=(AC[0-9a-f]{32})`" + `) + +type Detector struct{} +func (*Detector) ID() string { return "correlated" } +func (*Detector) Scan(data []byte) { + _ = primary.FindAllSubmatchIndex(data, -1) + _ = companion.FindAllSubmatchIndex(data, -1) + _ = metadata.FindAllSubmatchIndex(data, -1) +} +func (*Detector) PlaygroundPatternContract() detector.PlaygroundPatternContract { + return detector.PlaygroundPatternContract{ + Primary: []*regexp.Regexp{primary}, + RequiredNearby: []*regexp.Regexp{companion}, + ProximityBytes: nearby, + SameLogicalBlock: true, + RejectPlaceholders: true, + OneToOne: true, + } +} +` + out, dropped := extractDetectors(parseSnippet(t, src)) + if len(dropped) != 0 || len(out) != 1 { + t.Fatalf("unexpected extraction result: out=%+v dropped=%v", out, dropped) + } + entry := out[0] + if len(entry.Patterns) != 1 || entry.Patterns[0].Src != `SECRET=([^\s]+)` { + t.Fatalf("primary patterns = %+v", entry.Patterns) + } + if entry.Correlation == nil || len(entry.Correlation.RequiredNearby) != 1 { + t.Fatalf("correlation = %+v", entry.Correlation) + } + if entry.Correlation.RequiredNearby[0].Src != `SID=(SK[0-9a-f]{32})` || + entry.Correlation.ProximityBytes != 128 || !entry.Correlation.SameLogicalBlock || !entry.Correlation.RejectPlaceholders || !entry.Correlation.OneToOne { + t.Fatalf("correlation = %+v", entry.Correlation) + } +} + +func TestExtractDetectors_CorrelatedContractFailsClosed(t *testing.T) { + validFields := ` + Primary: []*regexp.Regexp{supported}, + RequiredNearby: []*regexp.Regexp{companion}, + ProximityBytes: nearby, + SameLogicalBlock: true, + RejectPlaceholders: true, + OneToOne: true,` + tests := map[string]string{ + "mixed supported and unsupported primary": `return detector.PlaygroundPatternContract{ + Primary: []*regexp.Regexp{supported, unsupported}, + RequiredNearby: []*regexp.Regexp{companion}, + ProximityBytes: nearby, + }`, + "unresolved required pattern": `return detector.PlaygroundPatternContract{ + Primary: []*regexp.Regexp{supported}, + RequiredNearby: []*regexp.Regexp{companion, unresolved}, + ProximityBytes: nearby, + }`, + "nonliteral boolean": `return detector.PlaygroundPatternContract{ + Primary: []*regexp.Regexp{supported}, + RequiredNearby: []*regexp.Regexp{companion}, + ProximityBytes: nearby, + SameLogicalBlock: configured, + }`, + "nested return": `if configured { + return detector.PlaygroundPatternContract{` + validFields + `} + } + return detector.PlaygroundPatternContract{` + validFields + `}`, + "wrong composite type": `return detector.OtherContract{` + validFields + `}`, + "duplicate field": `return detector.PlaygroundPatternContract{ + Primary: []*regexp.Regexp{supported}, + Primary: []*regexp.Regexp{supported}, + RequiredNearby: []*regexp.Regexp{companion}, + ProximityBytes: nearby, + }`, + "missing required field": `return detector.PlaygroundPatternContract{ + Primary: []*regexp.Regexp{supported}, + ProximityBytes: nearby, + }`, + "unknown field": `return detector.PlaygroundPatternContract{ + Primary: []*regexp.Regexp{supported}, + RequiredNearby: []*regexp.Regexp{companion}, + ProximityBytes: nearby, + FutureSemantic: true, + }`, + } + for name, body := range tests { + t.Run(name, func(t *testing.T) { + out, dropped := extractDetectors(parseSnippet(t, correlatedContractSnippet(body))) + if len(out) != 0 || len(dropped) != 1 || dropped[0] != "correlated-invalid" { + t.Fatalf("invalid contract must be dropped: out=%+v dropped=%v", out, dropped) + } + }) + } +} + +func TestBuildDetectors_StrictRejectsInvalidCorrelatedContract(t *testing.T) { + root := t.TempDir() + detectorDir := filepath.Join(root, "internal", "detector", "invalid") + if err := os.MkdirAll(detectorDir, 0o755); err != nil { + t.Fatal(err) + } + invalid := correlatedContractSnippet(`return detector.PlaygroundPatternContract{ + Primary: []*regexp.Regexp{supported, unsupported}, + RequiredNearby: []*regexp.Regexp{companion}, + ProximityBytes: nearby, + }`) + if err := os.WriteFile(filepath.Join(detectorDir, "invalid.go"), []byte(invalid), 0o644); err != nil { + t.Fatal(err) + } + if _, err := buildDetectors(root, t.TempDir(), true); err == nil { + t.Fatal("strict build accepted a partially representable correlated contract") + } +} + +func TestPlayground_TwilioRequiresNearbySID(t *testing.T) { + node, err := exec.LookPath("node") + if err != nil { + t.Fatal("node is required to verify browser detector contracts") + } + root, err := findRoot() + if err != nil { + t.Fatal(err) + } + script := ` +const fs = require("fs"); +const vm = require("vm"); +const elements = {}; +function element() { return { value: "", textContent: "", innerHTML: "", addEventListener() {}, focus() {} }; } +global.window = { addEventListener() {} }; +global.document = { + getElementById(id) { return elements[id] || (elements[id] = element()); }, + querySelectorAll() { return []; }, + addEventListener() {} +}; +global.navigator = {}; +global.setTimeout = function () {}; +vm.runInThisContext(fs.readFileSync(process.argv[1], "utf8")); +vm.runInThisContext(fs.readFileSync(process.argv[2], "utf8")); +const key = "SK" + "ab12cd34".repeat(4); +const account = "AC" + "ab12cd34".repeat(4); +const secret = "opaque/+Twilio.Secret=="; +function count(input) { return window.LW_PLAYGROUND_DETECT(input).filter(f => f.id === "twilio-api-key").length; } +function pairWithGap(gap) { return "TWILIO_API_KEY_SID=" + key + gap + "TWILIO_API_KEY_SECRET=" + secret; } +const cases = [ + ["bare key SID", "TWILIO_API_KEY_SID=" + key, 0], + ["bare account SID", "TWILIO_ACCOUNT_SID=" + account, 0], + ["unpaired secret", "TWILIO_API_KEY_SECRET=" + secret, 0], + ["paired", "TWILIO_API_KEY_SID=" + key + "\nTWILIO_API_KEY_SECRET=" + secret, 1], + ["prefixed pair", "MYAPP_TWILIO_API_KEY_SID=" + key + "\nMYAPP_TWILIO_API_KEY_SECRET=" + secret, 1], + ["generic X pair", "X_API_KEY_SID=" + key + "\nX_API_KEY_SECRET=" + secret, 1], + ["compose environment list", "environment:\n - TWILIO_API_KEY_SID=" + key + "\n - TWILIO_API_KEY_SECRET=" + secret, 1], + ["short opaque secret", "TWILIO_API_KEY_SID=" + key + "\nTWILIO_API_KEY_SECRET=x7-K", 1], + ["genuine value containing example", "TWILIO_API_KEY_SID=" + key + "\nTWILIO_API_KEY_SECRET=real-example-secret-value-42", 1], + ["placeholder", "TWILIO_API_KEY_SID=" + key + "\nTWILIO_API_KEY_SECRET=your_api_key_secret", 0], + ["canonical provider placeholder upper", "TWILIO_API_KEY_SID=" + key + "\nTWILIO_API_KEY_SECRET=YOUR_TWILIO_API_KEY_SECRET", 0], + ["canonical provider placeholder lower", "TWILIO_API_KEY_SID=" + key + "\nTWILIO_API_KEY_SECRET=your_twilio_api_key_secret", 0], + ["reference", "TWILIO_API_KEY_SID=" + key + "\nTWILIO_API_KEY_SECRET=${TWILIO_API_KEY_SECRET}", 0], + ["one SID is not reused", "TWILIO_API_KEY_SID=" + key + "\nTWILIO_API_KEY_SECRET=" + secret + "\nTWILIO_API_KEY_SECRET=second-opaque-secret", 1], + ["escaped quote is not truncated", "TWILIO_API_KEY_SID=" + key + "\nTWILIO_API_KEY_SECRET=\"opaque\\\"tail\"", 0], + ["hyphen suffixed SID", "TWILIO_API_KEY_SID=" + key + "-suffix\nTWILIO_API_KEY_SECRET=" + secret, 0], + ["dot suffixed SID", "TWILIO_API_KEY_SID=" + key + ".suffix\nTWILIO_API_KEY_SECRET=" + secret, 0], + ["quoted suffixed SID", "{\"ApiKeySid\":\"" + key + "-suffix\",\"ApiKeySecret\":\"" + secret + "\"}", 0], + ["quoted exact pair", "{\"ApiKeySid\":\"" + key + "\",\"ApiKeySecret\":\"" + secret + "\"}", 1], + ["ASCII 512 bytes", pairWithGap("\n" + "a".repeat(510) + " "), 1], + ["ASCII 513 bytes", pairWithGap("\n" + "a".repeat(511) + " "), 0], + ["two-byte Unicode 512 bytes", pairWithGap("\n" + "é".repeat(255) + " "), 1], + ["two-byte Unicode over limit", pairWithGap("\n" + "é".repeat(256) + " "), 0], + ["astral Unicode 512 bytes", pairWithGap("\n" + "🔐".repeat(127) + " "), 1], + ["astral Unicode 513 bytes", pairWithGap("\n" + "🔐".repeat(127) + " "), 0], + ["CRLF 512 bytes", pairWithGap("\r\n" + "a".repeat(509) + " "), 1], + ["CRLF 513 bytes", pairWithGap("\r\n" + "a".repeat(510) + " "), 0], + ["different block", "TWILIO_API_KEY_SID=" + key + "\n\nTWILIO_API_KEY_SECRET=" + secret, 0] +]; +for (const [name, input, want] of cases) { + const got = count(input); + if (got !== want) throw new Error(name + ": got " + got + ", want " + want); +} +var consecutive = ""; +for (var pairIndex = 1; pairIndex <= 5; pairIndex++) { + consecutive += "TWILIO_API_KEY_SID=SK" + pairIndex.toString(16).padStart(32, "0") + "\n"; + consecutive += "TWILIO_API_KEY_SECRET=opaque-secret-" + pairIndex + "-Q7mN2pL9rT4vW8xY\n"; +} +if (count(consecutive) !== 5) throw new Error("consecutive pairs were suppressed"); +const capCases = [ + ["ASCII exact cap", "a".repeat(64 * 1024), false], + ["ASCII over cap", "a".repeat(64 * 1024) + "x", true], + ["two-byte exact cap", "é".repeat(32 * 1024), false], + ["two-byte over cap", "é".repeat(32 * 1024) + "x", true], + ["astral exact cap", "🔐".repeat(16 * 1024), false], + ["astral over cap", "🔐".repeat(16 * 1024) + "x", true] +]; +for (const [name, input, wantTruncated] of capCases) { + const got = window.LW_PLAYGROUND_SCAN(input).truncated; + if (got !== wantTruncated) throw new Error(name + ": truncated=" + got + ", want " + wantTruncated); +} +` + cmd := exec.Command(node, "-e", script, + filepath.Join(root, "site", "js", "detectors.js"), + filepath.Join(root, "site", "js", "scanner.js")) + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("browser contract failed: %v\n%s", err, output) + } +} diff --git a/tools/site-build/documentation_links_test.go b/tools/site-build/documentation_links_test.go new file mode 100644 index 0000000..40e8285 --- /dev/null +++ b/tools/site-build/documentation_links_test.go @@ -0,0 +1,316 @@ +package main + +import ( + "fmt" + "io/fs" + "net/url" + "os" + "path/filepath" + "regexp" + "sort" + "strings" + "testing" + + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/text" +) + +func TestPublishedMarkdownLinksResolve(t *testing.T) { + root, err := findRoot() + if err != nil { + t.Fatal(err) + } + paths, err := publishedMarkdownPaths(root) + if err != nil { + t.Fatal(err) + } + if len(paths) == 0 { + t.Fatal("publishedMarkdownPaths() returned no documents") + } + validator := newMarkdownLinkValidator(root) + + for _, path := range paths { + source, readErr := os.ReadFile(path) + if readErr != nil { + t.Errorf("read %s: %v", path, readErr) + continue + } + for _, linkErr := range validator.validate(path, source) { + t.Error(linkErr) + } + } +} + +func TestValidateMarkdownLinks_CommonMarkMatrix(t *testing.T) { + root := t.TempDir() + docsDir := filepath.Join(root, "docs") + if err := os.MkdirAll(filepath.Join(docsDir, "dir"), 0o755); err != nil { + t.Fatal(err) + } + for _, name := range []string{"target.md", "image.png", "dir/space name.md", "dir/paren(name).md"} { + path := filepath.Join(docsDir, filepath.FromSlash(name)) + content := []byte("ok\n") + if name == "target.md" { + content = []byte("# Section\n") + } + if err := os.WriteFile(path, content, 0o644); err != nil { + t.Fatal(err) + } + } + path := filepath.Join(docsDir, "source.md") + + valid := strings.Join([]string{ + "# Local", + "", + "[inline](target.md \"title\")", + "[reference][target]", + "![image](image.png)", + "[angle]()", + "[parentheses](dir/paren(name).md)", + "[fragment](target.md#section)", + "[root relative](/docs/target.md)", + "[multiline](", + " target.md", + ")", + "[external](https://example.com/path)", + "[anchor](#local)", + "[site route](#/getting-started/installation)", + "", + "[target]: target.md", + }, "\n") + if err := os.WriteFile(path, []byte(valid), 0o644); err != nil { + t.Fatal(err) + } + if errs := validateMarkdownLinks(root, path, []byte(valid)); len(errs) != 0 { + t.Fatalf("valid CommonMark links returned errors: %v", errs) + } + + invalid := strings.Join([]string{ + "[missing inline](missing.md)", + "[missing reference][missing]", + "[repository escape](../../outside.md)", + "[missing fragment](target.md#absent)", + "", + "[missing]: absent.md", + }, "\n") + if errs := validateMarkdownLinks(root, path, []byte(invalid)); len(errs) != 4 { + t.Fatalf("invalid CommonMark links returned %d errors, want 4: %v", len(errs), errs) + } +} + +func TestPublishedMarkdownPaths_IgnoresNonDocumentationSymlinks(t *testing.T) { + root := t.TempDir() + if err := os.WriteFile(filepath.Join(root, "README.md"), []byte("# Docs\n"), 0o644); err != nil { + t.Fatal(err) + } + target := filepath.Join(root, "asset.txt") + if err := os.WriteFile(target, []byte("asset\n"), 0o644); err != nil { + t.Fatal(err) + } + if err := os.Symlink(target, filepath.Join(root, "asset-link")); err != nil { + t.Skipf("symlink unavailable: %v", err) + } + paths, err := publishedMarkdownPaths(root) + if err != nil { + t.Fatalf("non-documentation symlink caused false positive: %v", err) + } + if len(paths) != 1 || filepath.Base(paths[0]) != "README.md" { + t.Fatalf("publishedMarkdownPaths() = %v, want README.md only", paths) + } + if err := os.Symlink(filepath.Join(root, "README.md"), filepath.Join(root, "linked.md")); err != nil { + t.Skipf("Markdown symlink unavailable: %v", err) + } + if _, err := publishedMarkdownPaths(root); err == nil { + t.Fatal("publishedMarkdownPaths() accepted a Markdown symlink") + } +} + +func publishedMarkdownPaths(root string) ([]string, error) { + skipDirs := map[string]struct{}{ + ".git": {}, "node_modules": {}, "vendor": {}, "dist": {}, "review": {}, + } + var paths []string + err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if entry.Type()&os.ModeSymlink != 0 { + if strings.EqualFold(filepath.Ext(entry.Name()), ".md") { + return fmt.Errorf("documentation path must not be a symlink: %s", path) + } + return nil + } + if entry.IsDir() { + if path != root { + if _, skip := skipDirs[entry.Name()]; skip { + return filepath.SkipDir + } + } + return nil + } + if strings.EqualFold(filepath.Ext(entry.Name()), ".md") { + paths = append(paths, path) + } + return nil + }) + sort.Strings(paths) + return paths, err +} + +type markdownLinkValidator struct { + root string + anchorCache map[string]map[string]struct{} +} + +var rawHTMLAnchorPattern = regexp.MustCompile(`(?i)(?:id|name)[[:space:]]*=[[:space:]]*["']([^"']+)["']`) + +func newMarkdownLinkValidator(root string) *markdownLinkValidator { + return &markdownLinkValidator{root: filepath.Clean(root), anchorCache: make(map[string]map[string]struct{})} +} + +func validateMarkdownLinks(root, sourcePath string, source []byte) []error { + return newMarkdownLinkValidator(root).validate(sourcePath, source) +} + +func (v *markdownLinkValidator) validate(sourcePath string, source []byte) []error { + document := newMarkdown().Parser().Parse(text.NewReader(source)) + v.anchorCache[filepath.Clean(sourcePath)] = markdownAnchors(document, source) + var errors []error + _ = ast.Walk(document, func(node ast.Node, entering bool) (ast.WalkStatus, error) { + if !entering { + return ast.WalkContinue, nil + } + var destination []byte + switch n := node.(type) { + case *ast.Link: + destination = n.Destination + case *ast.Image: + destination = n.Destination + default: + return ast.WalkContinue, nil + } + if err := v.validateDestination(sourcePath, string(destination)); err != nil { + errors = append(errors, err) + } + return ast.WalkContinue, nil + }) + return errors +} + +func (v *markdownLinkValidator) validateDestination(sourcePath, rawDestination string) error { + destination := strings.TrimSpace(rawDestination) + if destination == "" || strings.HasPrefix(destination, "//") || strings.HasPrefix(destination, "#/") { + return nil + } + parsed, err := url.Parse(destination) + if err != nil { + return fmt.Errorf("%s has malformed link %q: %w", filepath.ToSlash(sourcePath), rawDestination, err) + } + if parsed.Scheme != "" { + return nil + } + target, err := v.resolveLocalLinkTarget(sourcePath, rawDestination, parsed) + if err != nil { + return err + } + if err := v.validateLocalLinkTarget(sourcePath, rawDestination, target); err != nil { + return err + } + return v.validateMarkdownFragment(sourcePath, rawDestination, target, parsed.Fragment) +} + +func (v *markdownLinkValidator) resolveLocalLinkTarget(sourcePath, rawDestination string, parsed *url.URL) (string, error) { + decodedPath, err := url.PathUnescape(parsed.Path) + if err != nil { + return "", fmt.Errorf("%s has malformed escaped link %q: %w", filepath.ToSlash(sourcePath), rawDestination, err) + } + var target string + if decodedPath == "" { + target = sourcePath + } else if strings.HasPrefix(decodedPath, "/") { + target = filepath.Join(v.root, filepath.FromSlash(strings.TrimPrefix(decodedPath, "/"))) + } else { + target = filepath.Join(filepath.Dir(sourcePath), filepath.FromSlash(decodedPath)) + } + target = filepath.Clean(target) + rel, err := filepath.Rel(v.root, target) + if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) { + return "", fmt.Errorf("%s has link %q outside the repository", filepath.ToSlash(sourcePath), rawDestination) + } + return target, nil +} + +func (v *markdownLinkValidator) validateLocalLinkTarget(sourcePath, rawDestination, target string) error { + info, err := os.Lstat(target) + if err != nil { + return fmt.Errorf("%s has unresolved link %q: %w", filepath.ToSlash(sourcePath), rawDestination, err) + } + if info.Mode()&os.ModeSymlink != 0 { + return fmt.Errorf("%s links to symlink %q; documentation targets must be repository-owned regular paths", filepath.ToSlash(sourcePath), rawDestination) + } + return nil +} + +func (v *markdownLinkValidator) validateMarkdownFragment(sourcePath, rawDestination, target, rawFragment string) error { + if rawFragment == "" || !strings.EqualFold(filepath.Ext(target), ".md") { + return nil + } + fragment, err := url.PathUnescape(rawFragment) + if err != nil { + return fmt.Errorf("%s has malformed escaped fragment in %q: %w", filepath.ToSlash(sourcePath), rawDestination, err) + } + anchors, err := v.anchorsFor(target) + if err != nil { + return fmt.Errorf("%s cannot inspect fragment target %q: %w", filepath.ToSlash(sourcePath), rawDestination, err) + } + if _, exists := anchors[fragment]; !exists { + return fmt.Errorf("%s has unresolved fragment %q in %s", filepath.ToSlash(sourcePath), fragment, filepath.ToSlash(target)) + } + return nil +} + +func (v *markdownLinkValidator) anchorsFor(path string) (map[string]struct{}, error) { + path = filepath.Clean(path) + if anchors, ok := v.anchorCache[path]; ok { + return anchors, nil + } + source, err := os.ReadFile(path) + if err != nil { + return nil, err + } + document := newMarkdown().Parser().Parse(text.NewReader(source)) + anchors := markdownAnchors(document, source) + v.anchorCache[path] = anchors + return anchors, nil +} + +func markdownAnchors(document ast.Node, source []byte) map[string]struct{} { + anchors := make(map[string]struct{}) + _ = ast.Walk(document, func(node ast.Node, entering bool) (ast.WalkStatus, error) { + if !entering { + return ast.WalkContinue, nil + } + if heading, ok := node.(*ast.Heading); ok { + if value, exists := heading.AttributeString("id"); exists { + switch id := value.(type) { + case []byte: + anchors[string(id)] = struct{}{} + case string: + anchors[id] = struct{}{} + } + } + } + var raw []byte + switch html := node.(type) { + case *ast.RawHTML: + raw = html.Segments.Value(source) + case *ast.HTMLBlock: + raw = html.Text(source) + } + for _, match := range rawHTMLAnchorPattern.FindAllSubmatch(raw, -1) { + anchors[string(match[1])] = struct{}{} + } + return ast.WalkContinue, nil + }) + return anchors +} diff --git a/tools/site-build/main.go b/tools/site-build/main.go index 38df710..9661588 100644 --- a/tools/site-build/main.go +++ b/tools/site-build/main.go @@ -1,5 +1,6 @@ -// Command site-build compiles the Leakwatch user manuals into the JavaScript -// data files consumed by the static documentation website. +// Command site-build compiles the Leakwatch user manuals and detector contracts +// into the static documentation website, and synchronizes release-version +// footers from internal/meta. // // Source layout: // @@ -17,11 +18,14 @@ package main import ( + "bytes" "encoding/json" "flag" "fmt" + "io" "os" "path/filepath" + "regexp" "strings" "github.com/yuin/goldmark" @@ -33,19 +37,25 @@ import ( // meta mirrors docs/user-manuals/_meta.yaml. type meta struct { - Languages []string `yaml:"languages"` - DefaultLanguage string `yaml:"default_language"` - Sections []struct { - ID string `yaml:"id"` - Icon string `yaml:"icon"` - Title map[string]string `yaml:"title"` - Pages []struct { - ID string `yaml:"id"` - Title map[string]string `yaml:"title"` - } `yaml:"pages"` - } `yaml:"sections"` + Languages []string `yaml:"languages"` + DefaultLanguage string `yaml:"default_language"` + Sections []metaSection `yaml:"sections"` } +type metaSection struct { + ID string `yaml:"id"` + Icon string `yaml:"icon"` + Title map[string]string `yaml:"title"` + Pages []metaPage `yaml:"pages"` +} + +type metaPage struct { + ID string `yaml:"id"` + Title map[string]string `yaml:"title"` +} + +var manualIDPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`) + // frontMatter is the YAML header of each Markdown page. type frontMatter struct { Title string `yaml:"title"` @@ -99,17 +109,30 @@ func run() error { if err != nil { return fmt.Errorf("read meta: %w", err) } - var m meta - if err := yaml.Unmarshal(metaBytes, &m); err != nil { + m, err := decodeManualMeta(metaBytes) + if err != nil { return fmt.Errorf("parse meta: %w", err) } if len(m.Languages) == 0 { return fmt.Errorf("no languages declared in _meta.yaml") } + manualsDir := filepath.Join(root, "docs", "user-manuals") + if err := validateManualContract(manualsDir, m, *strict); err != nil { + return err + } + + // Build every generated artifact outside the committed site tree. A parser, + // strict-missing-page, detector, or footer validation error can therefore + // never leave a partially regenerated workspace behind. + stageRoot, err := os.MkdirTemp("", "leakwatch-site-build-*") + if err != nil { + return fmt.Errorf("create site staging directory: %w", err) + } + defer func() { _ = os.RemoveAll(stageRoot) }() - outDir := filepath.Join(root, "site", "js", "manuals") + outDir := filepath.Join(stageRoot, "js", "manuals") if err := os.MkdirAll(outDir, 0o755); err != nil { - return fmt.Errorf("create output dir: %w", err) + return fmt.Errorf("create staging output dir: %w", err) } // Navigation index (language-independent). @@ -126,7 +149,6 @@ func run() error { } md := newMarkdown() - manualsDir := filepath.Join(root, "docs", "user-manuals") missing := 0 for _, lang := range m.Languages { @@ -161,7 +183,7 @@ func run() error { } // Compile the in-browser playground detector set from internal/detector. - jsDir := filepath.Join(root, "site", "js") + jsDir := filepath.Join(stageRoot, "js") nDet, err := buildDetectors(root, jsDir, *strict) if err != nil { return err @@ -171,6 +193,195 @@ func run() error { if missing > 0 && *strict { return fmt.Errorf("%d manual page(s) missing (strict mode)", missing) } + + releaseVersion, err := readReleaseVersion(root) + if err != nil { + return err + } + footerCount, err := commitStagedSite(root, stageRoot, releaseVersion) + if err != nil { + return err + } + fmt.Printf("site-build: published generated site and synced release footer %s (%d pages)\n", releaseVersion, footerCount) + return nil +} + +func decodeManualMeta(source []byte) (meta, error) { + var m meta + decoder := yaml.NewDecoder(bytes.NewReader(source)) + decoder.KnownFields(true) + if err := decoder.Decode(&m); err != nil { + return meta{}, err + } + var trailing any + if err := decoder.Decode(&trailing); err != io.EOF { + if err == nil { + return meta{}, fmt.Errorf("multiple YAML documents are not allowed") + } + return meta{}, err + } + return m, nil +} + +func validateManualContract(manualsDir string, m meta, strict bool) error { + languages, err := validateManualLanguages(m) + if err != nil { + return err + } + expected, err := expectedManualPages(m, languages) + if err != nil || !strict { + return err + } + if err := validateManualLanguageDirectories(manualsDir, languages); err != nil { + return err + } + for _, language := range m.Languages { + actual, err := actualManualPages(manualsDir, language, len(expected)) + if err != nil { + return err + } + if err := validateManualPageSet(language, expected, actual); err != nil { + return err + } + } + return nil +} + +func validateManualLanguages(m meta) (map[string]struct{}, error) { + languages := make(map[string]struct{}, len(m.Languages)) + for _, lang := range m.Languages { + if !manualIDPattern.MatchString(lang) { + return nil, fmt.Errorf("manual metadata contains invalid language ID %q", lang) + } + if _, duplicate := languages[lang]; duplicate { + return nil, fmt.Errorf("manual metadata contains duplicate language %q", lang) + } + languages[lang] = struct{}{} + } + if _, ok := languages[m.DefaultLanguage]; !ok { + return nil, fmt.Errorf("default language %q is not declared", m.DefaultLanguage) + } + if len(m.Sections) == 0 { + return nil, fmt.Errorf("manual metadata declares no sections") + } + return languages, nil +} + +func expectedManualPages(m meta, languages map[string]struct{}) (map[string]struct{}, error) { + expected := make(map[string]struct{}) + sectionIDs := make(map[string]struct{}, len(m.Sections)) + for _, section := range m.Sections { + if !manualIDPattern.MatchString(section.ID) { + return nil, fmt.Errorf("manual metadata contains invalid section ID %q", section.ID) + } + if _, duplicate := sectionIDs[section.ID]; duplicate { + return nil, fmt.Errorf("manual metadata contains duplicate section ID %q", section.ID) + } + sectionIDs[section.ID] = struct{}{} + if strings.TrimSpace(section.Icon) == "" { + return nil, fmt.Errorf("manual metadata section %q has no icon", section.ID) + } + if err := validateLocalizedTitles("section "+section.ID, section.Title, languages); err != nil { + return nil, err + } + if len(section.Pages) == 0 { + return nil, fmt.Errorf("manual metadata section %q declares no pages", section.ID) + } + if err := appendExpectedSectionPages(expected, section, languages); err != nil { + return nil, err + } + } + return expected, nil +} + +func appendExpectedSectionPages(expected map[string]struct{}, section metaSection, languages map[string]struct{}) error { + pageIDs := make(map[string]struct{}, len(section.Pages)) + for _, page := range section.Pages { + if !manualIDPattern.MatchString(page.ID) { + return fmt.Errorf("manual metadata section %q contains invalid page ID %q", section.ID, page.ID) + } + if _, duplicate := pageIDs[page.ID]; duplicate { + return fmt.Errorf("manual metadata section %q contains duplicate page ID %q", section.ID, page.ID) + } + pageIDs[page.ID] = struct{}{} + expected[filepath.ToSlash(filepath.Join(section.ID, page.ID+".md"))] = struct{}{} + if err := validateLocalizedTitles("page "+section.ID+"/"+page.ID, page.Title, languages); err != nil { + return err + } + } + return nil +} + +func validateManualLanguageDirectories(manualsDir string, languages map[string]struct{}) error { + entries, err := os.ReadDir(manualsDir) + if err != nil { + return fmt.Errorf("read manual source directory: %w", err) + } + for _, entry := range entries { + if entry.Type()&os.ModeSymlink != 0 { + return fmt.Errorf("manual source entry %q must not be a symlink", entry.Name()) + } + if !entry.IsDir() { + continue + } + if _, declared := languages[entry.Name()]; !declared { + return fmt.Errorf("manual source contains undeclared language directory %q", entry.Name()) + } + } + return nil +} + +func actualManualPages(manualsDir, language string, capacity int) (map[string]struct{}, error) { + langDir := filepath.Join(manualsDir, language) + actual := make(map[string]struct{}, capacity) + err := filepath.WalkDir(langDir, func(path string, entry os.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if entry.Type()&os.ModeSymlink != 0 { + return fmt.Errorf("manual source %s must not be a symlink", path) + } + if entry.IsDir() || !strings.EqualFold(filepath.Ext(entry.Name()), ".md") { + return nil + } + rel, relErr := filepath.Rel(langDir, path) + if relErr != nil { + return relErr + } + actual[filepath.ToSlash(rel)] = struct{}{} + return nil + }) + if err != nil { + return nil, fmt.Errorf("inspect %s manual sources: %w", language, err) + } + return actual, nil +} + +func validateManualPageSet(language string, expected, actual map[string]struct{}) error { + for path := range expected { + if _, ok := actual[path]; !ok { + return fmt.Errorf("manual page %s is missing for language %s", path, language) + } + } + for path := range actual { + if _, ok := expected[path]; !ok { + return fmt.Errorf("manual page %s for language %s is not declared in _meta.yaml", path, language) + } + } + return nil +} + +func validateLocalizedTitles(owner string, titles map[string]string, languages map[string]struct{}) error { + for lang := range languages { + if strings.TrimSpace(titles[lang]) == "" { + return fmt.Errorf("manual metadata %s has no %s title", owner, lang) + } + } + for lang := range titles { + if _, declared := languages[lang]; !declared { + return fmt.Errorf("manual metadata %s has a title for undeclared language %q", owner, lang) + } + } return nil } diff --git a/tools/site-build/main_test.go b/tools/site-build/main_test.go index 49db773..84bc001 100644 --- a/tools/site-build/main_test.go +++ b/tools/site-build/main_test.go @@ -1,6 +1,9 @@ package main import ( + "errors" + "os" + "path/filepath" "strings" "testing" ) @@ -100,3 +103,512 @@ func TestRenderMarkdown_TerminatedCalloutRendersNormally(t *testing.T) { t.Errorf("output missing content after the callout: %q", out) } } + +func TestValidateManualContract_FailsClosedOnMetadataAndSourceDrift(t *testing.T) { + validMeta := func() meta { + return meta{ + Languages: []string{"en", "tr"}, + DefaultLanguage: "en", + Sections: []metaSection{{ + ID: "getting-started", + Icon: "rocket", + Title: map[string]string{"en": "Getting Started", "tr": "Başlarken"}, + Pages: []metaPage{{ + ID: "introduction", + Title: map[string]string{"en": "Introduction", "tr": "Tanıtım"}, + }}, + }}, + } + } + writePage := func(t *testing.T, root, lang, relative string) { + t.Helper() + path := filepath.Join(root, lang, filepath.FromSlash(relative)) + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte("# page\n"), 0o644); err != nil { + t.Fatal(err) + } + } + validSources := func(t *testing.T) string { + t.Helper() + root := t.TempDir() + for _, lang := range []string{"en", "tr"} { + writePage(t, root, lang, "getting-started/introduction.md") + } + return root + } + + t.Run("valid exact source set", func(t *testing.T) { + requireNoError(t, validateManualContract(validSources(t), validMeta(), true)) + }) + + tests := []struct { + name string + mutate func(t *testing.T, root string, m *meta) + }{ + { + name: "English-only orphan", + mutate: func(t *testing.T, root string, _ *meta) { + writePage(t, root, "en", "getting-started/orphan.md") + }, + }, + { + name: "orphan in every language", + mutate: func(t *testing.T, root string, _ *meta) { + writePage(t, root, "en", "getting-started/orphan.md") + writePage(t, root, "tr", "getting-started/orphan.md") + }, + }, + { + name: "duplicate page ID", + mutate: func(_ *testing.T, _ string, m *meta) { + m.Sections[0].Pages = append(m.Sections[0].Pages, m.Sections[0].Pages[0]) + }, + }, + { + name: "duplicate section ID", + mutate: func(_ *testing.T, _ string, m *meta) { + m.Sections = append(m.Sections, m.Sections[0]) + }, + }, + { + name: "duplicate language ID", + mutate: func(_ *testing.T, _ string, m *meta) { + m.Languages = append(m.Languages, "en") + }, + }, + { + name: "missing translated title", + mutate: func(_ *testing.T, _ string, m *meta) { + delete(m.Sections[0].Pages[0].Title, "tr") + }, + }, + { + name: "undeclared translated title", + mutate: func(_ *testing.T, _ string, m *meta) { + m.Sections[0].Pages[0].Title["de"] = "Einführung" + }, + }, + { + name: "traversing page ID", + mutate: func(_ *testing.T, _ string, m *meta) { + m.Sections[0].Pages[0].ID = "../outside" + }, + }, + { + name: "missing declared source", + mutate: func(t *testing.T, root string, _ *meta) { + if err := os.Remove(filepath.Join(root, "tr", "getting-started", "introduction.md")); err != nil { + t.Fatal(err) + } + }, + }, + { + name: "invalid default language", + mutate: func(_ *testing.T, _ string, m *meta) { + m.DefaultLanguage = "de" + }, + }, + { + name: "undeclared language directory", + mutate: func(t *testing.T, root string, _ *meta) { + writePage(t, root, "de", "getting-started/introduction.md") + }, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + root := validSources(t) + m := validMeta() + tc.mutate(t, root, &m) + if err := validateManualContract(root, m, true); err == nil { + t.Fatal("validateManualContract() error = nil, want fail-closed error") + } + }) + } +} + +func TestDecodeManualMeta_FailsClosed(t *testing.T) { + valid := "languages: [en]\ndefault_language: en\nsections:\n - id: start\n icon: rocket\n title: {en: Start}\n pages:\n - id: intro\n title: {en: Intro}\n" + if _, err := decodeManualMeta([]byte(valid)); err != nil { + t.Fatalf("decodeManualMeta(valid) error = %v", err) + } + for name, source := range map[string]string{ + "unknown field": strings.Replace(valid, "default_language:", "default_languag:", 1), + "multiple documents": valid + "---\nlanguages: [tr]\n", + } { + t.Run(name, func(t *testing.T) { + if _, err := decodeManualMeta([]byte(source)); err == nil { + t.Fatal("decodeManualMeta() error = nil, want fail-closed error") + } + }) + } +} + +func requireNoError(t *testing.T, err error) { + t.Helper() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestReadReleaseVersion(t *testing.T) { + root := t.TempDir() + metaDir := filepath.Join(root, "internal", "meta") + if err := os.MkdirAll(metaDir, 0o755); err != nil { + t.Fatal(err) + } + path := filepath.Join(metaDir, "release.go") + if err := os.WriteFile(path, []byte("package meta\nconst ReleaseVersion = \"v2.3.4\"\n"), 0o644); err != nil { + t.Fatal(err) + } + + got, err := readReleaseVersion(root) + if err != nil { + t.Fatalf("readReleaseVersion() error = %v", err) + } + if got != "v2.3.4" { + t.Fatalf("readReleaseVersion() = %q, want v2.3.4", got) + } +} + +func TestReadReleaseVersion_FailsClosed(t *testing.T) { + tests := []struct { + name string + source string + }{ + {name: "missing", source: "package meta\nconst Other = \"v1.2.3\"\n"}, + {name: "computed", source: "package meta\nconst Prefix = \"v\"\nconst ReleaseVersion = Prefix + \"1.2.3\"\n"}, + {name: "prerelease", source: "package meta\nconst ReleaseVersion = \"v1.2.3-rc.1\"\n"}, + {name: "unsafe markup", source: "package meta\nconst ReleaseVersion = \"\"\n"}, + {name: "duplicate", source: "package meta\nconst ReleaseVersion = \"v1.2.3\"\nconst ReleaseVersion = \"v1.2.4\"\n"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + root := t.TempDir() + metaDir := filepath.Join(root, "internal", "meta") + if err := os.MkdirAll(metaDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(metaDir, "release.go"), []byte(tt.source), 0o644); err != nil { + t.Fatal(err) + } + if _, err := readReleaseVersion(root); err == nil { + t.Fatal("readReleaseVersion() error = nil, want fail-closed error") + } + }) + } +} + +func TestReplaceReleaseFooter(t *testing.T) { + input := []byte(`
    v0.9.0 · concept: redacted
    `) + want := `
    v1.7.0 · concept: redacted
    ` + got, err := replaceReleaseFooter(input, "v1.7.0") + if err != nil { + t.Fatalf("replaceReleaseFooter() error = %v", err) + } + if string(got) != want { + t.Fatalf("replaceReleaseFooter() = %q, want %q", got, want) + } +} + +func TestReplaceReleaseFooter_RejectsUnmanagedOrAmbiguousFooter(t *testing.T) { + tests := []struct { + name string + input string + }{ + {name: "unmanaged", input: `v1.6.0 · concept: redacted`}, + {name: "duplicate", input: releaseFooterOpen + `old` + releaseFooterOpen + `old`}, + {name: "unclosed", input: releaseFooterOpen + `old`}, + {name: "nested markup", input: releaseFooterOpen + `old`}, + {name: "extra unmanaged footer", input: releaseFooterOpen + `old · concept: redactedconcept: redacted`}, + {name: "only unmanaged footer text", input: `concept: redacted` + releaseFooterOpen + `old`}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, err := replaceReleaseFooter([]byte(tt.input), "v1.7.0"); err == nil { + t.Fatal("replaceReleaseFooter() error = nil, want fail-closed error") + } + }) + } +} + +func TestSyncSiteReleaseVersion_UpdatesEveryMarkedPage(t *testing.T) { + root := t.TempDir() + siteDir := filepath.Join(root, "site") + if err := os.MkdirAll(siteDir, 0o755); err != nil { + t.Fatal(err) + } + modes := map[string]os.FileMode{"index.html": 0o600, "docs.html": 0o644} + for name, mode := range modes { + content := releaseFooterOpen + "v0.1.0" + releaseFooterText + "" + if err := os.WriteFile(filepath.Join(siteDir, name), []byte(content), mode); err != nil { + t.Fatal(err) + } + } + + count, err := syncSiteReleaseVersion(root, "v1.7.0") + if err != nil { + t.Fatalf("syncSiteReleaseVersion() error = %v", err) + } + if count != 2 { + t.Fatalf("syncSiteReleaseVersion() count = %d, want 2", count) + } + for name, wantMode := range modes { + content, err := os.ReadFile(filepath.Join(siteDir, name)) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(content), "v1.7.0"+releaseFooterText) { + t.Fatalf("%s was not synchronized: %s", name, content) + } + info, err := os.Stat(filepath.Join(siteDir, name)) + if err != nil { + t.Fatal(err) + } + if got := info.Mode().Perm(); got != wantMode { + t.Fatalf("%s mode = %o, want %o", name, got, wantMode) + } + } +} + +func TestSyncSiteReleaseVersion_ValidatesAllPagesBeforeWriting(t *testing.T) { + root := t.TempDir() + siteDir := filepath.Join(root, "site") + if err := os.MkdirAll(siteDir, 0o755); err != nil { + t.Fatal(err) + } + goodPath := filepath.Join(siteDir, "a-good.html") + original := releaseFooterOpen + "v0.1.0" + releaseFooterText + "" + if err := os.WriteFile(goodPath, []byte(original), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(siteDir, "z-missing-footer.html"), []byte("
    no footer
    "), 0o644); err != nil { + t.Fatal(err) + } + + if _, err := syncSiteReleaseVersion(root, "v1.7.0"); err == nil { + t.Fatal("syncSiteReleaseVersion() error = nil, want missing-marker error") + } + content, err := os.ReadFile(goodPath) + if err != nil { + t.Fatal(err) + } + if string(content) != original { + t.Fatalf("earlier page was partially rewritten: got %q, want %q", content, original) + } + info, err := os.Stat(goodPath) + if err != nil { + t.Fatal(err) + } + if got := info.Mode().Perm(); got != 0o600 { + t.Fatalf("earlier page mode = %o, want 600", got) + } +} + +func TestCommitStagedSite_LateFooterErrorDoesNotPublishGeneratedFiles(t *testing.T) { + root := t.TempDir() + siteDir := filepath.Join(root, "site") + target := filepath.Join(siteDir, "js", "detectors.js") + if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(target, []byte("old generated output"), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(siteDir, "index.html"), []byte("
    missing footer
    "), 0o644); err != nil { + t.Fatal(err) + } + + stageRoot := t.TempDir() + staged := filepath.Join(stageRoot, "js", "detectors.js") + if err := os.MkdirAll(filepath.Dir(staged), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(staged, []byte("new generated output"), 0o644); err != nil { + t.Fatal(err) + } + + if _, err := commitStagedSite(root, stageRoot, "v1.7.0"); err == nil { + t.Fatal("commitStagedSite() error = nil, want missing-footer error") + } + content, err := os.ReadFile(target) + if err != nil { + t.Fatal(err) + } + if string(content) != "old generated output" { + t.Fatalf("generated target was partially published: %q", content) + } +} + +func TestCommitStagedSite_ReplaceFailureRollsBackEveryPublishedTarget(t *testing.T) { + fixture := prepareSiteRollbackFixture(t) + injectThirdReplaceFailure(t) + + if _, err := commitStagedSite(fixture.root, fixture.stageRoot, "v1.7.0"); err == nil { + t.Fatal("commitStagedSite() error = nil, want injected transaction failure") + } + assertSiteTransactionRolledBack(t, fixture) +} + +type siteRollbackFixture struct { + root, stageRoot, siteDir string + existingPath, newPath string + footerPath, originalFooter string +} + +func prepareSiteRollbackFixture(t *testing.T) siteRollbackFixture { + t.Helper() + root := t.TempDir() + siteDir := filepath.Join(root, "site") + jsDir := filepath.Join(siteDir, "js") + if err := os.MkdirAll(jsDir, 0o755); err != nil { + t.Fatal(err) + } + existingPath := filepath.Join(jsDir, "a-existing.js") + if err := os.WriteFile(existingPath, []byte("old generated output"), 0o600); err != nil { + t.Fatal(err) + } + newPath := filepath.Join(jsDir, "b-new.js") + footerPath := filepath.Join(siteDir, "index.html") + originalFooter := releaseFooterOpen + "v0.1.0" + releaseFooterText + "" + if err := os.WriteFile(footerPath, []byte(originalFooter), 0o640); err != nil { + t.Fatal(err) + } + + stageRoot := t.TempDir() + stagedJS := filepath.Join(stageRoot, "js") + if err := os.MkdirAll(stagedJS, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(stagedJS, "a-existing.js"), []byte("new existing output"), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(stagedJS, "b-new.js"), []byte("new file output"), 0o644); err != nil { + t.Fatal(err) + } + return siteRollbackFixture{ + root: root, stageRoot: stageRoot, siteDir: siteDir, + existingPath: existingPath, newPath: newPath, + footerPath: footerPath, originalFooter: originalFooter, + } +} + +func injectThirdReplaceFailure(t *testing.T) { + t.Helper() + originalReplace := replaceSiteFile + callCount := 0 + replaceSiteFile = func(oldPath, newPath string) error { + callCount++ + if callCount == 3 { + return errors.New("injected third replace failure") + } + return os.Rename(oldPath, newPath) + } + t.Cleanup(func() { replaceSiteFile = originalReplace }) +} + +func assertSiteTransactionRolledBack(t *testing.T, fixture siteRollbackFixture) { + t.Helper() + existing, err := os.ReadFile(fixture.existingPath) + if err != nil { + t.Fatal(err) + } + if string(existing) != "old generated output" { + t.Fatalf("existing generated target was not rolled back: %q", existing) + } + info, err := os.Stat(fixture.existingPath) + if err != nil { + t.Fatal(err) + } + if got := info.Mode().Perm(); got != 0o600 { + t.Fatalf("rolled-back target mode = %o, want 600", got) + } + if _, err := os.Stat(fixture.newPath); !os.IsNotExist(err) { + t.Fatalf("new target survived transaction rollback: err=%v", err) + } + footer, err := os.ReadFile(fixture.footerPath) + if err != nil { + t.Fatal(err) + } + if string(footer) != fixture.originalFooter { + t.Fatalf("footer changed after failed transaction: %q", footer) + } + for _, pattern := range []string{ + filepath.Join(fixture.siteDir, "js", ".*.tmp-*"), + filepath.Join(fixture.siteDir, ".*.tmp-*"), + } { + matches, globErr := filepath.Glob(pattern) + if globErr != nil { + t.Fatal(globErr) + } + if len(matches) != 0 { + t.Fatalf("transaction left temporary files: %v", matches) + } + } +} + +func TestPublishStagedSite_RejectsSymlinkTarget(t *testing.T) { + root := t.TempDir() + siteDir := filepath.Join(root, "site", "js") + if err := os.MkdirAll(siteDir, 0o755); err != nil { + t.Fatal(err) + } + external := filepath.Join(root, "external.txt") + if err := os.WriteFile(external, []byte("must remain unchanged"), 0o600); err != nil { + t.Fatal(err) + } + target := filepath.Join(siteDir, "detectors.js") + if err := os.Symlink(external, target); err != nil { + t.Skipf("symlink unavailable: %v", err) + } + + stageRoot := t.TempDir() + staged := filepath.Join(stageRoot, "js", "detectors.js") + if err := os.MkdirAll(filepath.Dir(staged), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(staged, []byte("replacement"), 0o644); err != nil { + t.Fatal(err) + } + if err := publishStagedSite(root, stageRoot); err == nil { + t.Fatal("publishStagedSite() error = nil, want symlink rejection") + } + content, err := os.ReadFile(external) + if err != nil { + t.Fatal(err) + } + if string(content) != "must remain unchanged" { + t.Fatalf("symlink target was modified: %q", content) + } +} + +func TestPublishStagedSite_RejectsSymlinkParentDirectory(t *testing.T) { + root := t.TempDir() + if err := os.MkdirAll(filepath.Join(root, "site"), 0o755); err != nil { + t.Fatal(err) + } + externalDir := filepath.Join(root, "external") + if err := os.MkdirAll(externalDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.Symlink(externalDir, filepath.Join(root, "site", "js")); err != nil { + t.Skipf("symlink unavailable: %v", err) + } + + stageRoot := t.TempDir() + staged := filepath.Join(stageRoot, "js", "detectors.js") + if err := os.MkdirAll(filepath.Dir(staged), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(staged, []byte("replacement"), 0o644); err != nil { + t.Fatal(err) + } + if err := publishStagedSite(root, stageRoot); err == nil { + t.Fatal("publishStagedSite() error = nil, want parent symlink rejection") + } + if _, err := os.Stat(filepath.Join(externalDir, "detectors.js")); !os.IsNotExist(err) { + t.Fatalf("external target exists after rejected publish: err=%v", err) + } +} diff --git a/tools/site-build/release.go b/tools/site-build/release.go new file mode 100644 index 0000000..ce88b32 --- /dev/null +++ b/tools/site-build/release.go @@ -0,0 +1,531 @@ +package main + +import ( + "bytes" + "errors" + "fmt" + "go/ast" + "go/parser" + "go/token" + "io/fs" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" +) + +const ( + releaseVersionConstant = "ReleaseVersion" + releaseFooterOpen = `` + releaseFooterText = ` · concept: redacted` +) + +var ( + stableReleasePattern = regexp.MustCompile(`^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$`) + replaceSiteFile = os.Rename +) + +// readReleaseVersion reads the canonical stable version without importing the +// parent Go module (tools/site-build intentionally has its own module). Keeping +// the value as a string literal also makes unsupported computed metadata fail +// closed instead of silently generating stale site content. +func readReleaseVersion(root string) (string, error) { + path := filepath.Join(root, "internal", "meta", "release.go") + file, err := parser.ParseFile(token.NewFileSet(), path, nil, 0) + if err != nil { + return "", fmt.Errorf("parse release metadata: %w", err) + } + + var found string + for _, decl := range file.Decls { + gen, ok := decl.(*ast.GenDecl) + if !ok || gen.Tok != token.CONST { + continue + } + for _, spec := range gen.Specs { + values, ok := spec.(*ast.ValueSpec) + if !ok { + continue + } + for i, name := range values.Names { + if name.Name != releaseVersionConstant { + continue + } + if found != "" { + return "", fmt.Errorf("%s is declared more than once", releaseVersionConstant) + } + if i >= len(values.Values) { + return "", fmt.Errorf("%s must have an explicit string literal", releaseVersionConstant) + } + literal, ok := values.Values[i].(*ast.BasicLit) + if !ok || literal.Kind != token.STRING { + return "", fmt.Errorf("%s must be a string literal", releaseVersionConstant) + } + version, err := strconv.Unquote(literal.Value) + if err != nil { + return "", fmt.Errorf("decode %s: %w", releaseVersionConstant, err) + } + if !stableReleasePattern.MatchString(version) { + return "", fmt.Errorf("%s %q is not a stable semantic version", releaseVersionConstant, version) + } + found = version + } + } + } + if found == "" { + return "", fmt.Errorf("%s not found in %s", releaseVersionConstant, path) + } + return found, nil +} + +type sitePageUpdate struct { + path string + content []byte + mode os.FileMode +} + +// syncSiteReleaseVersion validates every top-level site page before writing +// any of them, then atomically replaces each stale page. Requiring the marker +// on every page prevents a removed footer from silently escaping the canonical +// metadata pipeline. +func syncSiteReleaseVersion(root, version string) (int, error) { + updates, pageCount, err := collectSiteReleaseUpdates(root, version) + if err != nil { + return 0, err + } + if err := applySiteReleaseUpdates(filepath.Join(root, "site"), updates); err != nil { + return 0, err + } + return pageCount, nil +} + +func collectSiteReleaseUpdates(root, version string) ([]sitePageUpdate, int, error) { + if !stableReleasePattern.MatchString(version) { + return nil, 0, fmt.Errorf("refuse unsafe release version %q", version) + } + + siteDir := filepath.Join(root, "site") + if err := validateDirectoryTree(siteDir, siteDir); err != nil { + return nil, 0, fmt.Errorf("inspect site directory: %w", err) + } + pages, err := filepath.Glob(filepath.Join(siteDir, "*.html")) + if err != nil { + return nil, 0, fmt.Errorf("find site pages: %w", err) + } + if len(pages) == 0 { + return nil, 0, fmt.Errorf("no top-level site HTML pages found") + } + + updates := make([]sitePageUpdate, 0, len(pages)) + for _, path := range pages { + info, err := os.Lstat(path) + if err != nil { + return nil, 0, fmt.Errorf("inspect %s: %w", filepath.Base(path), err) + } + if !info.Mode().IsRegular() { + return nil, 0, fmt.Errorf("site page %s must be a regular file", filepath.Base(path)) + } + content, err := os.ReadFile(path) + if err != nil { + return nil, 0, fmt.Errorf("read %s: %w", filepath.Base(path), err) + } + updated, err := replaceReleaseFooter(content, version) + if err != nil { + return nil, 0, fmt.Errorf("release footer %s: %w", filepath.Base(path), err) + } + if bytes.Equal(content, updated) { + continue + } + updates = append(updates, sitePageUpdate{path: path, content: updated, mode: info.Mode().Perm()}) + } + + return updates, len(pages), nil +} + +type preparedSiteUpdate struct { + update sitePageUpdate + tempPath string + originalContent []byte + originalMode os.FileMode + existed bool +} + +// applySiteReleaseUpdates publishes a set of generated/footer files as one +// recoverable transaction. Every replacement is fully written and fsynced to +// a sibling temporary file before the first target changes. If a later atomic +// replace fails, earlier replacements are restored in reverse order; rollback +// failures are joined into the returned error instead of being hidden. +func applySiteReleaseUpdates(siteDir string, updates []sitePageUpdate) error { + if len(updates) == 0 { + return nil + } + if err := validateDirectoryTree(siteDir, siteDir); err != nil { + return fmt.Errorf("inspect site transaction root: %w", err) + } + normalized, err := normalizeSiteUpdates(siteDir, updates) + if err != nil { + return err + } + createdDirs, err := prepareSiteTargetDirectories(siteDir, normalized) + if err != nil { + return err + } + prepared, err := prepareSiteUpdates(normalized) + if err != nil { + cleanupPreparedSiteUpdates(prepared) + cleanupCreatedDirectories(createdDirs) + return err + } + if err := publishPreparedSiteUpdates(prepared); err != nil { + cleanupPreparedSiteUpdates(prepared) + cleanupCreatedDirectories(createdDirs) + return err + } + return nil +} + +func normalizeSiteUpdates(siteDir string, updates []sitePageUpdate) ([]sitePageUpdate, error) { + seenTargets := make(map[string]struct{}, len(updates)) + normalized := make([]sitePageUpdate, len(updates)) + for index, update := range updates { + update.path = filepath.Clean(update.path) + rel, err := filepath.Rel(siteDir, update.path) + if err != nil || rel == "." || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) { + return nil, fmt.Errorf("site transaction target %s escapes %s", update.path, siteDir) + } + if _, duplicate := seenTargets[update.path]; duplicate { + return nil, fmt.Errorf("site transaction contains duplicate target %s", update.path) + } + seenTargets[update.path] = struct{}{} + normalized[index] = update + } + return normalized, nil +} + +func prepareSiteTargetDirectories(siteDir string, updates []sitePageUpdate) ([]string, error) { + var createdDirs []string + for _, update := range updates { + created, err := ensureDirectoryTreeTracked(siteDir, filepath.Dir(update.path)) + if err != nil { + cleanupCreatedDirectories(createdDirs) + return nil, fmt.Errorf("prepare target directory for %s: %w", update.path, err) + } + createdDirs = append(createdDirs, created...) + } + return createdDirs, nil +} + +func prepareSiteUpdates(updates []sitePageUpdate) ([]preparedSiteUpdate, error) { + prepared := make([]preparedSiteUpdate, 0, len(updates)) + for _, update := range updates { + item, err := prepareSiteUpdate(update) + if err != nil { + return prepared, err + } + prepared = append(prepared, item) + } + return prepared, nil +} + +func prepareSiteUpdate(update sitePageUpdate) (preparedSiteUpdate, error) { + item := preparedSiteUpdate{update: update} + info, err := os.Lstat(update.path) + if err == nil { + if !info.Mode().IsRegular() { + return item, fmt.Errorf("site transaction target %s must be a regular file", update.path) + } + item.originalContent, err = os.ReadFile(update.path) + if err != nil { + return item, fmt.Errorf("snapshot site target %s: %w", update.path, err) + } + item.originalMode, item.existed = info.Mode().Perm(), true + } else if !os.IsNotExist(err) { + return item, fmt.Errorf("inspect site target %s: %w", update.path, err) + } + item.tempPath, err = prepareSiblingFile(update.path, update.content, update.mode) + if err != nil { + return item, fmt.Errorf("stage site target %s: %w", update.path, err) + } + return item, nil +} + +func publishPreparedSiteUpdates(prepared []preparedSiteUpdate) error { + for index := range prepared { + item := &prepared[index] + if err := replaceSiteFile(item.tempPath, item.update.path); err != nil { + publishErr := fmt.Errorf("publish site target %s: %w", item.update.path, err) + rollbackErr := rollbackSiteUpdates(prepared[:index]) + return errors.Join(publishErr, rollbackErr) + } + item.tempPath = "" + } + return nil +} + +func cleanupPreparedSiteUpdates(prepared []preparedSiteUpdate) { + for _, item := range prepared { + if item.tempPath != "" { + _ = os.Remove(item.tempPath) + } + } +} + +func prepareSiblingFile(target string, content []byte, mode os.FileMode) (string, error) { + tmp, err := os.CreateTemp(filepath.Dir(target), "."+filepath.Base(target)+".tmp-*") + if err != nil { + return "", err + } + tmpPath := tmp.Name() + failed := true + defer func() { + _ = tmp.Close() + if failed { + _ = os.Remove(tmpPath) + } + }() + if err := tmp.Chmod(mode.Perm()); err != nil { + return "", err + } + if _, err := tmp.Write(content); err != nil { + return "", err + } + if err := tmp.Sync(); err != nil { + return "", err + } + if err := tmp.Close(); err != nil { + return "", err + } + failed = false + return tmpPath, nil +} + +func rollbackSiteUpdates(committed []preparedSiteUpdate) error { + var rollbackErrors []error + for index := len(committed) - 1; index >= 0; index-- { + item := committed[index] + if item.existed { + if err := writeFileAtomic(item.update.path, item.originalContent, item.originalMode); err != nil { + rollbackErrors = append(rollbackErrors, fmt.Errorf("restore %s: %w", item.update.path, err)) + } + continue + } + if err := os.Remove(item.update.path); err != nil && !os.IsNotExist(err) { + rollbackErrors = append(rollbackErrors, fmt.Errorf("remove new target %s: %w", item.update.path, err)) + } + } + return errors.Join(rollbackErrors...) +} + +func replaceReleaseFooter(content []byte, version string) ([]byte, error) { + text := string(content) + markerCount := strings.Count(text, releaseFooterOpen) + if markerCount == 0 { + return nil, fmt.Errorf("missing data-release-version marker") + } + if markerCount != 1 { + return nil, fmt.Errorf("expected one data-release-version marker, found %d", markerCount) + } + footerTextCount := strings.Count(text, "concept: redacted") + if footerTextCount > 1 { + return nil, fmt.Errorf("expected at most one release footer text, found %d", footerTextCount) + } + + start := strings.Index(text, releaseFooterOpen) + bodyStart := start + len(releaseFooterOpen) + closeOffset := strings.Index(text[bodyStart:], "") + if closeOffset < 0 { + return nil, fmt.Errorf("marked release footer has no closing span") + } + closingSpan := bodyStart + closeOffset + if strings.ContainsAny(text[bodyStart:closingSpan], "<>") { + return nil, fmt.Errorf("marked release footer body must be plain text") + } + if footerTextCount == 1 && !strings.Contains(text[bodyStart:closingSpan], "concept: redacted") { + return nil, fmt.Errorf("release footer text exists outside the marked span") + } + wanted := text[:bodyStart] + version + releaseFooterText + text[closingSpan:] + return []byte(wanted), nil +} + +func writeFileAtomic(path string, content []byte, mode os.FileMode) error { + tmp, err := os.CreateTemp(filepath.Dir(path), "."+filepath.Base(path)+".tmp-*") + if err != nil { + return err + } + tmpPath := tmp.Name() + defer func() { + _ = tmp.Close() + _ = os.Remove(tmpPath) + }() + + if err := tmp.Chmod(mode.Perm()); err != nil { + return err + } + if _, err := tmp.Write(content); err != nil { + return err + } + if err := tmp.Sync(); err != nil { + return err + } + if err := tmp.Close(); err != nil { + return err + } + if err := os.Rename(tmpPath, path); err != nil { + return err + } + return nil +} + +// commitStagedSite validates the release footer before publishing any staged +// JavaScript. All parsing/rendering/detector work therefore completes outside +// the committed site tree, and a late validation error leaves it untouched. +func commitStagedSite(root, stageRoot, version string) (int, error) { + footerUpdates, pageCount, err := collectSiteReleaseUpdates(root, version) + if err != nil { + return 0, err + } + generatedUpdates, err := collectStagedSiteUpdates(root, stageRoot) + if err != nil { + return 0, err + } + updates := append(generatedUpdates, footerUpdates...) + if err := applySiteReleaseUpdates(filepath.Join(root, "site"), updates); err != nil { + return 0, err + } + return pageCount, nil +} + +func publishStagedSite(root, stageRoot string) error { + updates, err := collectStagedSiteUpdates(root, stageRoot) + if err != nil { + return err + } + return applySiteReleaseUpdates(filepath.Join(root, "site"), updates) +} + +func collectStagedSiteUpdates(root, stageRoot string) ([]sitePageUpdate, error) { + var stagedFiles []string + err := filepath.WalkDir(stageRoot, func(path string, entry fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if entry.IsDir() { + return nil + } + if entry.Type()&os.ModeSymlink != 0 || !entry.Type().IsRegular() { + return fmt.Errorf("staged site entry %s must be a regular file", path) + } + stagedFiles = append(stagedFiles, path) + return nil + }) + if err != nil { + return nil, fmt.Errorf("inspect staged site: %w", err) + } + if len(stagedFiles) == 0 { + return nil, fmt.Errorf("staged site contains no generated files") + } + + siteDir := filepath.Join(root, "site") + updates := make([]sitePageUpdate, 0, len(stagedFiles)) + for _, stagedPath := range stagedFiles { + rel, err := filepath.Rel(stageRoot, stagedPath) + if err != nil || rel == "." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) { + return nil, fmt.Errorf("resolve staged site path %s", stagedPath) + } + target := filepath.Join(siteDir, rel) + if err := validateDirectoryTree(siteDir, filepath.Dir(target)); err != nil { + return nil, fmt.Errorf("inspect generated site directory for %s: %w", target, err) + } + + mode := os.FileMode(0o644) + info, err := os.Lstat(target) + switch { + case err == nil: + if !info.Mode().IsRegular() { + return nil, fmt.Errorf("generated site target %s must be a regular file", target) + } + mode = info.Mode().Perm() + case !os.IsNotExist(err): + return nil, fmt.Errorf("inspect generated site target %s: %w", target, err) + } + content, err := os.ReadFile(stagedPath) + if err != nil { + return nil, fmt.Errorf("read staged site file %s: %w", stagedPath, err) + } + updates = append(updates, sitePageUpdate{path: target, content: content, mode: mode}) + } + return updates, nil +} + +func validateDirectoryTree(base, target string) error { + rel, err := filepath.Rel(base, target) + if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) { + return fmt.Errorf("directory %s escapes base %s", target, base) + } + + current := base + parts := []string{} + if rel != "." { + parts = strings.Split(rel, string(filepath.Separator)) + } + for i := -1; i < len(parts); i++ { + if i >= 0 { + current = filepath.Join(current, parts[i]) + } + info, err := os.Lstat(current) + if os.IsNotExist(err) { + if i == -1 { + return fmt.Errorf("base directory %s does not exist", base) + } + return nil + } + if err != nil { + return err + } + if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() { + return fmt.Errorf("%s must be a real directory", current) + } + } + return nil +} + +func ensureDirectoryTreeTracked(base, target string) ([]string, error) { + if err := validateDirectoryTree(base, target); err != nil { + return nil, err + } + rel, err := filepath.Rel(base, target) + if err != nil { + return nil, err + } + current := base + if rel == "." { + return nil, nil + } + var created []string + for _, part := range strings.Split(rel, string(filepath.Separator)) { + current = filepath.Join(current, part) + if err := os.Mkdir(current, 0o755); err == nil { + created = append(created, current) + } else if !os.IsExist(err) { + cleanupCreatedDirectories(created) + return nil, err + } + info, err := os.Lstat(current) + if err != nil { + cleanupCreatedDirectories(created) + return nil, err + } + if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() { + cleanupCreatedDirectories(created) + return nil, fmt.Errorf("%s must be a real directory", current) + } + } + return created, nil +} + +func cleanupCreatedDirectories(paths []string) { + for index := len(paths) - 1; index >= 0; index-- { + _ = os.Remove(paths[index]) + } +} diff --git a/vscode/.vscodeignore b/vscode/.vscodeignore index 180d215..b223df6 100644 --- a/vscode/.vscodeignore +++ b/vscode/.vscodeignore @@ -10,3 +10,4 @@ eslint.config.mjs package-lock.json **/*.ts **/*.map +images/icon.svg diff --git a/vscode/CHANGELOG.md b/vscode/CHANGELOG.md index b8d2701..ab0f2f8 100644 --- a/vscode/CHANGELOG.md +++ b/vscode/CHANGELOG.md @@ -36,6 +36,8 @@ file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1. output-buffer overflow. - ESLint flat config, unit tests (Node test runner) for the parsing and path-resolution helpers, and a committed `package-lock.json`. +- A 256×256 Marketplace icon, lockfile-pinned VSIX packaging command, CI package + artifact, and a protected manual Marketplace publishing workflow. ## [0.1.0] diff --git a/vscode/README.md b/vscode/README.md index 31a31cf..4828699 100644 --- a/vscode/README.md +++ b/vscode/README.md @@ -54,9 +54,19 @@ npm run watch # development mode with auto-rebuild ```bash npm run compile # build for production -npx @vscode/vsce package # create .vsix package +npm run package:vsix # create leakwatch-.vsix ``` +The committed 256×256 Marketplace icon is rendered from `images/icon.svg`. +CI packages and uploads a VSIX for every extension change. Marketplace +publication is a separate manual, `main`-only workflow. Administrators must +configure the `vscode-marketplace` environment with required reviewers, +prevent-self-review, protected-branch deployment policy, and an environment-only +`VSCE_PAT`. Add a short-lived, repository-restricted `VSCODE_ENV_AUDIT_TOKEN` +with only **Environments: read**; the workflow uses it to prove that `VSCE_PAT` +exists in that environment, audits the policies through the GitHub API, and +fails closed before publication when either the policy or secret provenance drifts. + ## License MIT — see [LICENSE](../LICENSE) for details. diff --git a/vscode/images/icon.png b/vscode/images/icon.png new file mode 100644 index 0000000..8fdcebc Binary files /dev/null and b/vscode/images/icon.png differ diff --git a/vscode/images/icon.svg b/vscode/images/icon.svg new file mode 100644 index 0000000..de1a582 --- /dev/null +++ b/vscode/images/icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/vscode/package-lock.json b/vscode/package-lock.json index a08d362..164ede0 100644 --- a/vscode/package-lock.json +++ b/vscode/package-lock.json @@ -14,6 +14,7 @@ "@types/vscode": "^1.85.0", "@typescript-eslint/eslint-plugin": "^8.20.0", "@typescript-eslint/parser": "^8.20.0", + "@vscode/vsce": "3.9.2", "eslint": "^9.18.0", "ts-loader": "^9.5.0", "typescript": "^5.3.0", @@ -25,6 +26,216 @@ "vscode": "^1.85.0" } }, + "node_modules/@azu/format-text": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@azu/format-text/-/format-text-1.0.2.tgz", + "integrity": "sha512-Swi4N7Edy1Eqq82GxgEECXSSLyn6GOb5htRFPzBDdUkECGXtlf12ynO5oJSpWKPwCaUssOu7NfhDcCWpIC6Ywg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@azu/style-format": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@azu/style-format/-/style-format-1.0.1.tgz", + "integrity": "sha512-AHcTojlNBdD/3/KxIKlg8sxIWHfOtQszLvOpagLTO+bjC3u7SAszu1lf//u7JJC50aUSH+BVWDD/KvaA6Gfn5g==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "@azu/format-text": "^1.0.1" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.2.0.tgz", + "integrity": "sha512-fNAjWnA/nZ2jz31kxR/AqRaUT8ewHBw/WuBIosK0moMy1C9e5ValbDfFdIxJzVOOYaYkV/b2F1S4H/aHiqfVQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.11.0.tgz", + "integrity": "sha512-IUZydyTUkDnYdstOW9pFOOUQlBjAepK5teihDE3x6yxsPJs/hsAaaYpeGxdxrgtOiJbBKSjKW7MDk7AEhb4LRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.11.0.tgz", + "integrity": "sha512-JjQWO6akOck45PH/XBrxzsQGAiKrfFl4m5iggJ0ItMIz5omRufOXWpqCPpdjKN3vKDzlSUvFjaMb7Zwf0gvAdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.25.0.tgz", + "integrity": "sha512-bMs8ekJLjX8wPV+9IPBges1SLPyuDtE9g5gLDWOpxzKcoOFQnpLGkbcT1tdw3FaAmDS1gnPmMmJ6y/T5B96kIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.4.0.tgz", + "integrity": "sha512-eGwxD0AtncrxeBM4tG8R55Pc3rdX1hNW2WibJAgYpCVA6E93mvvVH+LcssoVjOBrSKWS55yEIHsk0X8ctHmfOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.14.0.tgz", + "integrity": "sha512-9n2pWK61veAuN0V20t9lOuoV4CFMdyAZ1ygZzvBGk/pBBJRib/PjL9PLXa/aI2CcPpyHfqVsxxqLCYl6uZlfDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.1.tgz", + "integrity": "sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^5.5.0", + "@azure/msal-node": "^5.1.0", + "open": "^10.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.4.0.tgz", + "integrity": "sha512-rbAE25KUfjU/s3XHUdJgceoCP5dEOpMx85J04kF+QMdta73XkuG9JGHHinch+XIoKpBdqljin+KqURpJriSzLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.18.0.tgz", + "integrity": "sha512-SPTeHYZghdEdRddJzNjhH+CI5MSQtquNYwGJnYXfOHIBRXCmrWimBS85OhwXpXFIlrCtNTbBPm5mPAWRNEoktA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.12.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "16.12.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.12.0.tgz", + "integrity": "sha512-hgLgfRdbG2AmhXPygebf1KYJEvse86+ZZLWufdiTKaGRYEUqOzHdlf6AS1IiuUCHWbynkgbHc451jSNkbfhWlg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.5.0.tgz", + "integrity": "sha512-A/2WIsuH0vsC6JVkkafjS4kHpi2LDR4AzDT0kJ+oIRtXYeYtvGQ2pwN2X88thQPhSek+82ela3MprsKXWQRrhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.12.0", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -87,9 +298,9 @@ "license": "MIT" }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", - "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -168,9 +379,9 @@ "license": "MIT" }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", - "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -354,848 +565,1979 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "20.19.43", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", - "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" } }, - "node_modules/@types/vscode": { - "version": "1.125.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.125.0.tgz", - "integrity": "sha512-0icm/ZQAaism87P0ekHqi4/Ju9du+Tm0RUW+y7vqRsxY2cY0FNRX1nAnaW7nT6npPt2tfHiheZ55Zm9UhqonFA==", + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">= 8" + } }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz", - "integrity": "sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==", + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.64.0", - "@typescript-eslint/type-utils": "8.64.0", - "@typescript-eslint/utils": "8.64.0", - "@typescript-eslint/visitor-keys": "8.64.0", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.5.0" + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.64.0", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "node": ">= 8" } }, - "node_modules/@typescript-eslint/parser": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.64.0.tgz", - "integrity": "sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==", + "node_modules/@secretlint/config-creator": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/config-creator/-/config-creator-10.2.2.tgz", + "integrity": "sha512-BynOBe7Hn3LJjb3CqCHZjeNB09s/vgf0baBaHVw67w7gHF0d25c3ZsZ5+vv8TgwSchRdUCRrbbcq5i2B1fJ2QQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.64.0", - "@typescript-eslint/types": "8.64.0", - "@typescript-eslint/typescript-estree": "8.64.0", - "@typescript-eslint/visitor-keys": "8.64.0", - "debug": "^4.4.3" + "@secretlint/types": "^10.2.2" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "node": ">=20.0.0" } }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.64.0.tgz", - "integrity": "sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==", + "node_modules/@secretlint/config-loader": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/config-loader/-/config-loader-10.2.2.tgz", + "integrity": "sha512-ndjjQNgLg4DIcMJp4iaRD6xb9ijWQZVbd9694Ol2IszBIbGPPkwZHzJYKICbTBmh6AH/pLr0CiCaWdGJU7RbpQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.64.0", - "@typescript-eslint/types": "^8.64.0", - "debug": "^4.4.3" + "@secretlint/profiler": "^10.2.2", + "@secretlint/resolver": "^10.2.2", + "@secretlint/types": "^10.2.2", + "ajv": "^8.17.1", + "debug": "^4.4.1", + "rc-config-loader": "^4.1.3" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "node": ">=20.0.0" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.64.0.tgz", - "integrity": "sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==", + "node_modules/@secretlint/config-loader/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.64.0", - "@typescript-eslint/visitor-keys": "8.64.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz", - "integrity": "sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==", + "node_modules/@secretlint/config-loader/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@secretlint/core": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/core/-/core-10.2.2.tgz", + "integrity": "sha512-6rdwBwLP9+TO3rRjMVW1tX+lQeo5gBbxl1I5F8nh8bgGtKwdlCMhMKsBWzWg1ostxx/tIG7OjZI0/BxsP8bUgw==", "dev": true, "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "dependencies": { + "@secretlint/profiler": "^10.2.2", + "@secretlint/types": "^10.2.2", + "debug": "^4.4.1", + "structured-source": "^4.0.0" }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.64.0.tgz", - "integrity": "sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==", + "node_modules/@secretlint/formatter": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/formatter/-/formatter-10.2.2.tgz", + "integrity": "sha512-10f/eKV+8YdGKNQmoDUD1QnYL7TzhI2kzyx95vsJKbEa8akzLAR5ZrWIZ3LbcMmBLzxlSQMMccRmi05yDQ5YDA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.64.0", - "@typescript-eslint/typescript-estree": "8.64.0", - "@typescript-eslint/utils": "8.64.0", - "debug": "^4.4.3", - "ts-api-utils": "^2.5.0" + "@secretlint/resolver": "^10.2.2", + "@secretlint/types": "^10.2.2", + "@textlint/linter-formatter": "^15.2.0", + "@textlint/module-interop": "^15.2.0", + "@textlint/types": "^15.2.0", + "chalk": "^5.4.1", + "debug": "^4.4.1", + "pluralize": "^8.0.0", + "strip-ansi": "^7.1.0", + "table": "^6.9.0", + "terminal-link": "^4.0.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "node": ">=20.0.0" } }, - "node_modules/@typescript-eslint/types": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.64.0.tgz", - "integrity": "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==", + "node_modules/@secretlint/formatter/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "dev": true, "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz", - "integrity": "sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==", + "node_modules/@secretlint/node": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/node/-/node-10.2.2.tgz", + "integrity": "sha512-eZGJQgcg/3WRBwX1bRnss7RmHHK/YlP/l7zOQsrjexYt6l+JJa5YhUmHbuGXS94yW0++3YkEJp0kQGYhiw1DMQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.64.0", - "@typescript-eslint/tsconfig-utils": "8.64.0", - "@typescript-eslint/types": "8.64.0", - "@typescript-eslint/visitor-keys": "8.64.0", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" + "@secretlint/config-loader": "^10.2.2", + "@secretlint/core": "^10.2.2", + "@secretlint/formatter": "^10.2.2", + "@secretlint/profiler": "^10.2.2", + "@secretlint/source-creator": "^10.2.2", + "@secretlint/types": "^10.2.2", + "debug": "^4.4.1", + "p-map": "^7.0.3" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "node": ">=20.0.0" } }, - "node_modules/@typescript-eslint/utils": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.64.0.tgz", - "integrity": "sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==", + "node_modules/@secretlint/profiler": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/profiler/-/profiler-10.2.2.tgz", + "integrity": "sha512-qm9rWfkh/o8OvzMIfY8a5bCmgIniSpltbVlUVl983zDG1bUuQNd1/5lUEeWx5o/WJ99bXxS7yNI4/KIXfHexig==", + "dev": true, + "license": "MIT" + }, + "node_modules/@secretlint/resolver": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/resolver/-/resolver-10.2.2.tgz", + "integrity": "sha512-3md0cp12e+Ae5V+crPQYGd6aaO7ahw95s28OlULGyclyyUtf861UoRGS2prnUrKh7MZb23kdDOyGCYb9br5e4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@secretlint/secretlint-formatter-sarif": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-formatter-sarif/-/secretlint-formatter-sarif-10.2.2.tgz", + "integrity": "sha512-ojiF9TGRKJJw308DnYBucHxkpNovDNu1XvPh7IfUp0A12gzTtxuWDqdpuVezL7/IP8Ua7mp5/VkDMN9OLp1doQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.64.0", - "@typescript-eslint/types": "8.64.0", - "@typescript-eslint/typescript-estree": "8.64.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "node-sarif-builder": "^3.2.0" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.64.0.tgz", - "integrity": "sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==", + "node_modules/@secretlint/secretlint-rule-no-dotenv": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-no-dotenv/-/secretlint-rule-no-dotenv-10.2.2.tgz", + "integrity": "sha512-KJRbIShA9DVc5Va3yArtJ6QDzGjg3PRa1uYp9As4RsyKtKSSZjI64jVca57FZ8gbuk4em0/0Jq+uy6485wxIdg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.64.0", - "eslint-visitor-keys": "^5.0.0" + "@secretlint/types": "^10.2.2" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=20.0.0" } }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "node_modules/@secretlint/secretlint-rule-preset-recommend": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-preset-recommend/-/secretlint-rule-preset-recommend-10.2.2.tgz", + "integrity": "sha512-K3jPqjva8bQndDKJqctnGfwuAxU2n9XNCPtbXVI5JvC7FnQiNg/yWlQPbMUlBXtBoBGFYp08A94m6fvtc9v+zA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=20.0.0" } }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "node_modules/@secretlint/source-creator": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/source-creator/-/source-creator-10.2.2.tgz", + "integrity": "sha512-h6I87xJfwfUTgQ7irWq7UTdq/Bm1RuQ/fYhA3dtTIAop5BwSFmZyrchph4WcoEvbN460BWKmk4RYSvPElIIvxw==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + "@secretlint/types": "^10.2.2", + "istextorbinary": "^9.5.0" + }, + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "node_modules/@secretlint/types": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/types/-/types-10.2.2.tgz", + "integrity": "sha512-Nqc90v4lWCXyakD6xNyNACBJNJ0tNCwj2WNk/7ivyacYHxiITVgmLUFXTBOeCdy79iz6HtN9Y31uw/jbLrdOAg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "node_modules/@textlint/ast-node-types": { + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.8.0.tgz", + "integrity": "sha512-5CiH9COYmovWmExQgs7763DzX6Gy9zjkjJ7JxCC95wyTcjwQn/8poNF6fv3qzRlmx8CRRde8DHr9FcgAAiPzgw==", "dev": true, "license": "MIT" }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "node_modules/@textlint/linter-formatter": { + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-15.8.0.tgz", + "integrity": "sha512-+oU3A235NATv6Lzi4xa4kJ65PuNJlIxesaO4AvDhDWA9FWm7y4XKWaoQCW1esgaQQ6dwnUiFKArQ8TcJ86mC4w==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" + "@azu/format-text": "^1.0.2", + "@azu/style-format": "^1.0.1", + "@textlint/module-interop": "15.8.0", + "@textlint/resolver": "15.8.0", + "@textlint/types": "15.8.0", + "debug": "^4.4.3", + "js-yaml": "^4.3.0", + "lodash": "^4.18.1", + "pluralize": "^2.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "table": "^6.9.0", + "text-table": "^0.2.0" + }, + "engines": { + "node": ">=20.18.0" } }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "node_modules/@textlint/linter-formatter/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@textlint/linter-formatter/node_modules/pluralize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-2.0.0.tgz", + "integrity": "sha512-TqNZzQCD4S42De9IfnnBvILN7HAW7riLqsCyp8lgjXeysyPlX5HhqKAcJHHHb9XskE4/a+7VGC9zzx8Ls0jOAw==", "dev": true, "license": "MIT" }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "node_modules/@textlint/linter-formatter/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "node_modules/@textlint/module-interop": { + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-15.8.0.tgz", + "integrity": "sha512-rt+OR1WYGoLOY8HkA/aBPrqufF6yUUEsKEAh7XohTsT3lp9IyZFT6zOIbjul9P4FAzsmSPkcrYjVx3Bz/IUfkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/resolver": { + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-15.8.0.tgz", + "integrity": "sha512-E88tzfX3K8Jykk+38aJ9cy8RquD8ABVOPTO2rFEESq0wcg8x6/ypdAS8ZgR7OKiGqlRF0hkO/m5PbQwVfKM3VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/types": { + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/@textlint/types/-/types-15.8.0.tgz", + "integrity": "sha512-Anhc6y5736YIsvqae0U6k0YmB2M/QVHkEeOv2aydAn/WIkdI69dCOiDbe3/+RagS3qstFTSFWJzNRA2lUjv19w==", "dev": true, "license": "MIT", "dependencies": { - "@xtuc/ieee754": "^1.2.0" + "@textlint/ast-node-types": "15.8.0" } }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@xtuc/long": "4.2.2" - } + "license": "MIT" }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, "license": "MIT" }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" + "undici-types": "~6.21.0" } }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } + "license": "MIT" }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "node_modules/@types/sarif": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.7.tgz", + "integrity": "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==", "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" - } + "license": "MIT" }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "node_modules/@types/vscode": { + "version": "1.125.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.125.0.tgz", + "integrity": "sha512-0icm/ZQAaism87P0ekHqi4/Ju9du+Tm0RUW+y7vqRsxY2cY0FNRX1nAnaW7nT6npPt2tfHiheZ55Zm9UhqonFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz", + "integrity": "sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.64.0", + "@typescript-eslint/type-utils": "8.64.0", + "@typescript-eslint/utils": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.64.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "node_modules/@typescript-eslint/parser": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.64.0.tgz", + "integrity": "sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" + "@typescript-eslint/scope-manager": "8.64.0", + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/typescript-estree": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@webpack-cli/configtest": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", - "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.64.0.tgz", + "integrity": "sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==", "dev": true, "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.64.0", + "@typescript-eslint/types": "^8.64.0", + "debug": "^4.4.3" + }, "engines": { - "node": ">=14.15.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@webpack-cli/info": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", - "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.64.0.tgz", + "integrity": "sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==", "dev": true, "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0" + }, "engines": { - "node": ">=14.15.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz", + "integrity": "sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@webpack-cli/serve": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", - "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.64.0.tgz", + "integrity": "sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/typescript-estree": "8.64.0", + "@typescript-eslint/utils": "8.64.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.64.0.tgz", + "integrity": "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz", + "integrity": "sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.64.0", + "@typescript-eslint/tsconfig-utils": "8.64.0", + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.64.0.tgz", + "integrity": "sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.64.0", + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/typescript-estree": "8.64.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.64.0.tgz", + "integrity": "sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.64.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.8.tgz", + "integrity": "sha512-bLMpVcWZNzq6lYOybwFwOAR1IXKcHnhUNqYeHjl1bET/qE3jFPFH+p8Wrh3rU4xwdnifPxmKNESBYnvnmc75aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@vscode/vsce": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.9.2.tgz", + "integrity": "sha512-XSxMosEEDO6vLxELAHVkwmhC0qe0ijZni2jB9Rcs8kQsW4lhTDQ/wMzmwFs/buotAWSnpmUp/dRWD2ufG3UYKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/identity": "^4.1.0", + "@secretlint/node": "^10.1.2", + "@secretlint/secretlint-formatter-sarif": "^10.1.2", + "@secretlint/secretlint-rule-no-dotenv": "^10.1.2", + "@secretlint/secretlint-rule-preset-recommend": "^10.1.2", + "@vscode/vsce-sign": "^2.0.0", + "azure-devops-node-api": "^12.5.0", + "chalk": "^4.1.2", + "cheerio": "^1.0.0-rc.9", + "cockatiel": "^3.1.2", + "commander": "^12.1.0", + "form-data": "^4.0.0", + "glob": "^13.0.6", + "hosted-git-info": "^4.0.2", + "jsonc-parser": "^3.2.0", + "leven": "^3.1.0", + "markdown-it": "^14.1.0", + "mime": "^1.3.4", + "minimatch": "^10.2.2", + "parse-semver": "^1.1.1", + "read": "^1.0.7", + "secretlint": "^10.1.2", + "semver": "^7.5.2", + "tmp": "^0.2.3", + "typed-rest-client": "^1.8.4", + "url-join": "^4.0.1", + "xml2js": "^0.5.0", + "yauzl": "^3.2.1", + "yazl": "^2.2.2" + }, + "bin": { + "vsce": "vsce" + }, + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "keytar": "^7.7.0" + } + }, + "node_modules/@vscode/vsce-sign": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.0.9.tgz", + "integrity": "sha512-8IvaRvtFyzUnGGl3f5+1Cnor3LqaUWvhaUjAYO8Y39OUYlOf3cRd+dowuQYLpZcP3uwSG+mURwjEBOSq4SOJ0g==", + "dev": true, + "hasInstallScript": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optionalDependencies": { + "@vscode/vsce-sign-alpine-arm64": "2.0.6", + "@vscode/vsce-sign-alpine-x64": "2.0.6", + "@vscode/vsce-sign-darwin-arm64": "2.0.6", + "@vscode/vsce-sign-darwin-x64": "2.0.6", + "@vscode/vsce-sign-linux-arm": "2.0.6", + "@vscode/vsce-sign-linux-arm64": "2.0.6", + "@vscode/vsce-sign-linux-x64": "2.0.6", + "@vscode/vsce-sign-win32-arm64": "2.0.6", + "@vscode/vsce-sign-win32-x64": "2.0.6" + } + }, + "node_modules/@vscode/vsce-sign-alpine-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.6.tgz", + "integrity": "sha512-wKkJBsvKF+f0GfsUuGT0tSW0kZL87QggEiqNqK6/8hvqsXvpx8OsTEc3mnE1kejkh5r+qUyQ7PtF8jZYN0mo8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-alpine-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.6.tgz", + "integrity": "sha512-YoAGlmdK39vKi9jA18i4ufBbd95OqGJxRvF3n6ZbCyziwy3O+JgOpIUPxv5tjeO6gQfx29qBivQ8ZZTUF2Ba0w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.6.tgz", + "integrity": "sha512-5HMHaJRIQuozm/XQIiJiA0W9uhdblwwl2ZNDSSAeXGO9YhB9MH5C4KIHOmvyjUnKy4UCuiP43VKpIxW1VWP4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.6.tgz", + "integrity": "sha512-25GsUbTAiNfHSuRItoQafXOIpxlYj+IXb4/qarrXu7kmbH94jlm5sdWSCKrrREs8+GsXF1b+l3OB7VJy5jsykw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.6.tgz", + "integrity": "sha512-UndEc2Xlq4HsuMPnwu7420uqceXjs4yb5W8E2/UkaHBB9OWCwMd3/bRe/1eLe3D8kPpxzcaeTyXiK3RdzS/1CA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.6.tgz", + "integrity": "sha512-cfb1qK7lygtMa4NUl2582nP7aliLYuDEVpAbXJMkDq1qE+olIw/es+C8j1LJwvcRq1I2yWGtSn3EkDp9Dq5FdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.6.tgz", + "integrity": "sha512-/olerl1A4sOqdP+hjvJ1sbQjKN07Y3DVnxO4gnbn/ahtQvFrdhUi0G1VsZXDNjfqmXw57DmPi5ASnj/8PGZhAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-win32-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.6.tgz", + "integrity": "sha512-ivM/MiGIY0PJNZBoGtlRBM/xDpwbdlCWomUWuLmIxbi1Cxe/1nooYrEQoaHD8ojVRgzdQEUzMsRbyF5cJJgYOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/vsce-sign-win32-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.6.tgz", + "integrity": "sha512-mgth9Kvze+u8CruYMmhHw6Zgy3GRX2S+Ed5oSokDEK5vPEwGGKnmuXua9tmFhomeAnhgJnL4DCna3TiNuGrBTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/vsce/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/azure-devops-node-api": { + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", + "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", + "dev": true, + "license": "MIT", + "dependencies": { + "tunnel": "0.0.6", + "typed-rest-client": "^1.8.4" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.43", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz", + "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binaryextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-6.11.0.tgz", + "integrity": "sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "editions": "^6.21.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/boundary": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/boundary/-/boundary-2.0.0.tgz", + "integrity": "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", + "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001803", + "electron-to-chromium": "^1.5.389", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001805", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001805.tgz", + "integrity": "sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/cockatiel": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", + "integrity": "sha512-gfrHV6ZPkquExvMh9IOkKsBzNDk6sDuZ6DdBGUBkvFnTCqCxzpuq48RySgP0AnaqQkw2zynOFj9yly6T1Q2G5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" + "dependencies": { + "color-name": "~1.1.4" }, - "peerDependenciesMeta": { - "webpack-dev-server": { - "optional": true - } + "engines": { + "node": ">=7.0.0" } }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, - "license": "BSD-3-Clause" + "license": "MIT" }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true, - "license": "Apache-2.0" + "license": "MIT" }, - "node_modules/acorn": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", - "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "delayed-stream": "~1.0.0" }, "engines": { - "node": ">=0.4.0" + "node": ">= 0.8" } }, - "node_modules/acorn-import-phases": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", - "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=10.13.0" + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, - "peerDependencies": { - "acorn": "^8.14.0" + "engines": { + "node": ">= 8" } }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" } }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "url": "https://github.com/sponsors/fb55" } }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^8.0.0" + "ms": "^2.1.3" }, - "peerDependencies": { - "ajv": "^8.0.0" + "engines": { + "node": ">=6.0" }, "peerDependenciesMeta": { - "ajv": { + "supports-color": { "optional": true } } }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, "license": "MIT" }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" }, "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", "dev": true, "license": "MIT", "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.43", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz", - "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" + "node": ">=18" }, - "engines": { - "node": ">=6.0.0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, "engines": { - "node": "18 || 20 || >=22" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/browserslist": { - "version": "4.28.6", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", - "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==", + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.10.42", - "caniuse-lite": "^1.0.30001803", - "electron-to-chromium": "^1.5.389", - "node-releases": "^2.0.51", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + "node": ">=0.4.0" } }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, - "license": "MIT" + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6" + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, - "node_modules/caniuse-lite": { - "version": "1.0.30001805", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001805.tgz", - "integrity": "sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA==", + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, + "funding": [ { "type": "github", - "url": "https://github.com/sponsors/ai" + "url": "https://github.com/sponsors/fb55" } ], - "license": "CC-BY-4.0" + "license": "BSD-2-Clause" }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "domelementtype": "^2.3.0" }, "engines": { - "node": ">=10" + "node": ">= 4" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0" + "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" }, - "engines": { - "node": ">=6" + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" }, "engines": { - "node": ">=7.0.0" + "node": ">= 0.4" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", "dev": true, - "license": "MIT" + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "node_modules/editions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/editions/-/editions-6.22.0.tgz", + "integrity": "sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ==", "dev": true, - "license": "MIT" + "license": "Artistic-2.0", + "dependencies": { + "version-range": "^4.15.0" + }, + "engines": { + "ecmascript": ">= es5", + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "node_modules/electron-to-chromium": { + "version": "1.5.392", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.392.tgz", + "integrity": "sha512-1yQq3VQCZRwsnYc67Oc+1fge6Lwtn0hzi6zmEVkB61Zx21kTbwJAW4dFLadl5Rc1tKhG/kSpYXnfiAhu0f0a1g==", "dev": true, - "license": "MIT" + "license": "ISC" }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT" }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", "dev": true, "license": "MIT", "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" }, - "engines": { - "node": ">= 8" + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" } }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "once": "^1.4.0" } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.392", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.392.tgz", - "integrity": "sha512-1yQq3VQCZRwsnYc67Oc+1fge6Lwtn0hzi6zmEVkB61Zx21kTbwJAW4dFLadl5Rc1tKhG/kSpYXnfiAhu0f0a1g==", - "dev": true, - "license": "ISC" - }, "node_modules/enhanced-resolve": { "version": "5.24.2", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.2.tgz", @@ -1210,6 +2552,19 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/envinfo": { "version": "7.21.0", "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", @@ -1223,6 +2578,29 @@ "node": ">=4" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-errors": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", @@ -1240,6 +2618,35 @@ "dev": true, "license": "MIT" }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -1361,9 +2768,9 @@ "license": "MIT" }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", - "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -1494,6 +2901,17 @@ "node": ">=0.8.x" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1501,6 +2919,36 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -1516,9 +2964,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", - "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", "dev": true, "funding": [ { @@ -1542,6 +2990,16 @@ "node": ">= 4.9.1" } }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -1573,6 +3031,19 @@ "node": ">=16.0.0" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -1600,35 +3071,140 @@ "flat": "cli.js" } }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/fs-extra": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz", + "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, "license": "MIT", "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": ">=16" + "node": ">= 0.4" } }, - "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", "dev": true, - "license": "ISC" + "license": "MIT", + "optional": true }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/glob-parent": { @@ -1657,6 +3233,40 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globby": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -1674,6 +3284,35 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", @@ -1687,6 +3326,115 @@ "node": ">= 0.4" } }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause", + "optional": true + }, "node_modules/ignore": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", @@ -1744,6 +3492,35 @@ "node": ">=0.8.19" } }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC", + "optional": true + }, "node_modules/interpret": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", @@ -1770,6 +3547,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1780,6 +3573,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -1793,6 +3596,35 @@ "node": ">=0.10.0" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -1806,6 +3638,22 @@ "node": ">=0.10.0" } }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1823,6 +3671,24 @@ "node": ">=0.10.0" } }, + "node_modules/istextorbinary": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-9.5.0.tgz", + "integrity": "sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "binaryextensions": "^6.11.0", + "editions": "^6.21.0", + "textextensions": "^6.11.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, "node_modules/jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", @@ -1854,10 +3720,17 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/js-yaml": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", - "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", "dev": true, "funding": [ { @@ -1877,26 +3750,118 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "dev": true, + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "node_modules/keytar": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", "dev": true, - "license": "MIT" + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" + } }, "node_modules/keyv": { "version": "4.5.4", @@ -1918,6 +3883,16 @@ "node": ">=0.10.0" } }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -1932,6 +3907,26 @@ "node": ">= 0.8.0" } }, + "node_modules/linkify-it": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/loader-runner": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", @@ -1962,6 +3957,55 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -1969,6 +4013,78 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/markdown-it": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz", + "integrity": "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.5.0", + "linkify-it": "^5.0.2", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdurl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", + "dev": true, + "license": "MIT" + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -1976,6 +4092,56 @@ "dev": true, "license": "MIT" }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -1986,6 +4152,43 @@ "node": ">= 0.6" } }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", @@ -2002,6 +4205,17 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minimizer-webpack-plugin": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.6.1.tgz", @@ -2063,35 +4277,195 @@ } } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true, + "license": "ISC" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/node-sarif-builder": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/node-sarif-builder/-/node-sarif-builder-3.4.0.tgz", + "integrity": "sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sarif": "^2.1.7", + "fs-extra": "^11.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/normalize-package-data": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", "dev": true, - "license": "MIT" + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, - "license": "MIT" + "license": "ISC", + "optional": true, + "dependencies": { + "wrappy": "1" + } }, - "node_modules/node-releases": { - "version": "2.0.51", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", - "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", "dev": true, "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, "engines": { "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/optionator": { @@ -2144,6 +4518,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.6.tgz", + "integrity": "sha512-I4Prw6ivkd6p8PiYR1tXASOAOBzIJwu0TB7fqaX0c/8c3QAehNYmX57EijyGGGBt3c/BIowGwV03RVBtXvHEVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -2167,6 +4554,97 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-semver": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", + "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.1.0" + } + }, + "node_modules/parse-semver/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2194,6 +4672,53 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-type": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2247,60 +4772,262 @@ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc-config-loader": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/rc-config-loader/-/rc-config-loader-4.1.4.tgz", + "integrity": "sha512-3GiwEzklkbXTDp52UR5nT8iXgYAx1V9ZG/kDZT7p60u2GCv2XTwQq4NzinMoMpNtXhmt3WkhYXcj6HH8HdwCEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "js-yaml": "^4.1.1", + "json5": "^2.2.3", + "require-from-string": "^2.0.2" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "optional": true, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "node_modules/read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "p-try": "^2.0.0" + "mute-stream": "~0.0.4" }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.8" } }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/read-pkg": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", + "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", "dev": true, "license": "MIT", "dependencies": { - "p-limit": "^2.2.0" + "@types/normalize-package-data": "^2.4.3", + "normalize-package-data": "^6.0.0", + "parse-json": "^8.0.0", + "type-fest": "^4.6.0", + "unicorn-magic": "^0.1.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "node_modules/read-pkg/node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.8.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, "engines": { - "node": ">=6" + "node": ">= 6" } }, "node_modules/rechoir": { @@ -2381,6 +5108,92 @@ "node": ">=4" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, "node_modules/schema-utils": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", @@ -2438,6 +5251,28 @@ "dev": true, "license": "MIT" }, + "node_modules/secretlint": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/secretlint/-/secretlint-10.2.2.tgz", + "integrity": "sha512-xVpkeHV/aoWe4vP4TansF622nBEImzCY73y/0042DuJ29iKIaqgoJ8fGxre3rVSHHbxar4FdJobmTnLp9AU0eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/config-creator": "^10.2.2", + "@secretlint/formatter": "^10.2.2", + "@secretlint/node": "^10.2.2", + "@secretlint/profiler": "^10.2.2", + "debug": "^4.4.1", + "globby": "^14.1.0", + "read-pkg": "^9.0.1" + }, + "bin": { + "secretlint": "bin/secretlint.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/semver": { "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", @@ -2487,6 +5322,162 @@ "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, "node_modules/source-map": { "version": "0.7.6", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", @@ -2515,7 +5506,108 @@ "dev": true, "license": "BSD-3-Clause", "engines": { - "node": ">=0.10.0" + "node": ">=0.10.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/strip-json-comments": { @@ -2531,6 +5623,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/structured-source": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/structured-source/-/structured-source-4.0.0.tgz", + "integrity": "sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boundary": "^2.0.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -2544,6 +5646,23 @@ "node": ">=8" } }, + "node_modules/supports-hyperlinks": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -2557,6 +5676,70 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/table/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/table/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/table/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tapable": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", @@ -2571,6 +5754,55 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/terminal-link": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-4.0.0.tgz", + "integrity": "sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "supports-hyperlinks": "^3.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/terser": { "version": "5.49.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.49.0.tgz", @@ -2590,6 +5822,29 @@ "node": ">=10" } }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/textextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-6.11.0.tgz", + "integrity": "sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "editions": "^6.21.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -2607,6 +5862,29 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -2645,6 +5923,37 @@ } } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -2658,6 +5967,31 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-rest-client": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", + "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "qs": "^6.9.1", + "tunnel": "0.0.6", + "underscore": "^1.12.1" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -2696,6 +6030,30 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -2703,6 +6061,29 @@ "dev": true, "license": "MIT" }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -2744,6 +6125,45 @@ "punycode": "^2.1.0" } }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/version-range": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/version-range/-/version-range-4.15.0.tgz", + "integrity": "sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg==", + "dev": true, + "license": "Artistic-2.0", + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, "node_modules/watchpack": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", @@ -2908,6 +6328,30 @@ "node": ">=4.0" } }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -2941,6 +6385,84 @@ "node": ">=0.10.0" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yauzl": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", + "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/vscode/package.json b/vscode/package.json index bc332ae..8a06a45 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -4,6 +4,11 @@ "description": "Detect leaked secrets in your code with Leakwatch", "version": "0.1.0", "publisher": "HodeTech", + "icon": "images/icon.png", + "galleryBanner": { + "color": "#111316", + "theme": "dark" + }, "license": "MIT", "repository": { "type": "git", @@ -70,7 +75,12 @@ "leakwatch.minSeverity": { "type": "string", "default": "low", - "enum": ["low", "medium", "high", "critical"], + "enum": [ + "low", + "medium", + "high", + "critical" + ], "description": "Minimum severity level to report" }, "leakwatch.showInlineHints": { @@ -102,6 +112,7 @@ "scripts": { "vscode:prepublish": "npm run compile", "compile": "webpack --mode production", + "package:vsix": "vsce package --no-dependencies", "watch": "webpack --mode development --watch", "lint": "eslint src", "pretest": "tsc -p ./", @@ -113,6 +124,7 @@ "@types/vscode": "^1.85.0", "@typescript-eslint/eslint-plugin": "^8.20.0", "@typescript-eslint/parser": "^8.20.0", + "@vscode/vsce": "3.9.2", "eslint": "^9.18.0", "ts-loader": "^9.5.0", "typescript": "^5.3.0",