diff --git a/.claude/skills/caveman-commit/README.md b/.claude/skills/caveman-commit/README.md new file mode 100644 index 0000000..d5aee01 --- /dev/null +++ b/.claude/skills/caveman-commit/README.md @@ -0,0 +1,44 @@ +# caveman-commit + +Terse Conventional Commits. Why over what. + +## What it does + +Generates commit messages in Conventional Commits format. Subject ≤50 chars, hard cap 72. Imperative mood. Body only when the *why* is non-obvious or there are breaking changes. No AI attribution, no "this commit does X", no emoji unless the project uses them. Body always required for breaking changes, security fixes, data migrations, and reverts — future debuggers need the context. + +Outputs only the message. Does not stage, commit, or amend. + +## How to invoke + +``` +/caveman-commit +``` + +Also triggers on phrases like "write a commit", "commit message", "generate commit". + +## Example output + +Diff: new endpoint for user profile. + +``` +feat(api): add GET /users/:id/profile + +Mobile client needs profile data without the full user payload +to reduce LTE bandwidth on cold-launch screens. + +Closes #128 +``` + +Diff: breaking API rename. + +``` +feat(api)!: rename /v1/orders to /v1/checkout + +BREAKING CHANGE: clients on /v1/orders must migrate to /v1/checkout +before 2026-06-01. Old route returns 410 after that date. +``` + +## See also + +- [`SKILL.md`](./SKILL.md) — full LLM-facing instructions +- [Caveman README](../../README.md) — repo overview diff --git a/.claude/skills/caveman-commit/SKILL.md b/.claude/skills/caveman-commit/SKILL.md new file mode 100644 index 0000000..b9999e3 --- /dev/null +++ b/.claude/skills/caveman-commit/SKILL.md @@ -0,0 +1,65 @@ +--- +name: caveman-commit +description: > + Ultra-compressed commit message generator. Cuts noise from commit messages while preserving + intent and reasoning. Conventional Commits format. Subject ≤50 chars, body only when "why" + isn't obvious. Use when user says "write a commit", "commit message", "generate commit", + "/commit", or invokes /caveman-commit. Auto-triggers when staging changes. +--- + +Write commit messages terse and exact. Conventional Commits format. No fluff. Why over what. + +## Rules + +**Subject line:** +- `(): ` — `` optional +- Types: `feat`, `fix`, `refactor`, `perf`, `docs`, `test`, `chore`, `build`, `ci`, `style`, `revert` +- Imperative mood: "add", "fix", "remove" — not "added", "adds", "adding" +- ≤50 chars when possible, hard cap 72 +- No trailing period +- Match project convention for capitalization after the colon + +**Body (only if needed):** +- Skip entirely when subject is self-explanatory +- Add body only for: non-obvious *why*, breaking changes, migration notes, linked issues +- Wrap at 72 chars +- Bullets `-` not `*` +- Reference issues/PRs at end: `Closes #42`, `Refs #17` + +**What NEVER goes in:** +- "This commit does X", "I", "we", "now", "currently" — the diff says what +- "As requested by..." — use Co-authored-by trailer +- "Generated with Claude Code" or any AI attribution — unless the user's own rule requires an `Assisted-by`/AI-attribution trailer, then add it as a trailer +- Emoji (unless project convention requires) +- Restating the file name when scope already says it + +## Examples + +Diff: new endpoint for user profile with body explaining the why +- ❌ "feat: add a new endpoint to get user profile information from the database" +- ✅ + ``` + feat(api): add GET /users/:id/profile + + Mobile client needs profile data without the full user payload + to reduce LTE bandwidth on cold-launch screens. + + Closes #128 + ``` + +Diff: breaking API change +- ✅ + ``` + feat(api)!: rename /v1/orders to /v1/checkout + + BREAKING CHANGE: clients on /v1/orders must migrate to /v1/checkout + before 2026-06-01. Old route returns 410 after that date. + ``` + +## Auto-Clarity + +Always include body for: breaking changes, security fixes, data migrations, anything reverting a prior commit. Never compress these into subject-only — future debuggers need the context. + +## Boundaries + +Only generates the commit message. Does not run `git commit`, does not stage files, does not amend. Output the message as a code block ready to paste. "stop caveman-commit" or "normal mode": revert to verbose commit style. diff --git a/.claude/skills/caveman-help/README.md b/.claude/skills/caveman-help/README.md new file mode 100644 index 0000000..5841256 --- /dev/null +++ b/.claude/skills/caveman-help/README.md @@ -0,0 +1,38 @@ +# caveman-help + +Quick-reference card. One shot, no mode change. + +## What it does + +Prints a cheat sheet of all caveman modes, sibling skills, deactivation triggers, and how to set the default mode via env var or config file. One-shot display — does not flip the active mode, write flag files, or persist anything. Use when you forget the slash commands. + +## How to invoke + +``` +/caveman-help +``` + +Also triggers on "caveman help", "what caveman commands", "how do I use caveman". + +## Example output + +``` +Modes: + /caveman full (default) + /caveman lite lighter + /caveman ultra extreme + /caveman wenyan classical Chinese + +Skills: + /caveman-commit terse Conventional Commits + /caveman-review one-line PR comments + /caveman-stats session token savings + +Deactivate: + "stop caveman" or "normal mode" +``` + +## See also + +- [`SKILL.md`](./SKILL.md) — full reference card +- [Caveman README](../../README.md) — repo overview diff --git a/.claude/skills/caveman-help/SKILL.md b/.claude/skills/caveman-help/SKILL.md new file mode 100644 index 0000000..ce1156f --- /dev/null +++ b/.claude/skills/caveman-help/SKILL.md @@ -0,0 +1,63 @@ +--- +name: caveman-help +description: > + Quick-reference card for all caveman modes, skills, and commands. + One-shot display, not a persistent mode. Trigger: /caveman-help, + "caveman help", "what caveman commands", "how do I use caveman". +--- + +# Caveman Help + +Display this reference card when invoked. One-shot — do NOT change mode, write flag files, or persist anything. Output in caveman style. + +## Modes + +| Mode | Trigger | What change | +|------|---------|-------------| +| **Lite** | `/caveman lite` | Drop filler. Keep sentence structure. | +| **Full** | `/caveman` | Drop articles, filler, pleasantries, hedging. Fragments OK. Default. | +| **Ultra** | `/caveman ultra` | Extreme compression. Bare fragments. Tables over prose. | +| **Wenyan-Lite** | `/caveman wenyan-lite` | Classical Chinese style, light compression. | +| **Wenyan-Full** | `/caveman wenyan` | Full 文言文. Maximum classical terseness. | +| **Wenyan-Ultra** | `/caveman wenyan-ultra` | Extreme. Ancient scholar on a budget. | + +Mode stick until changed or session end. + +## Skills + +| Skill | Trigger | What it do | +|-------|---------|-----------| +| **caveman-commit** | `/caveman-commit` | Terse commit messages. Conventional Commits. ≤50 char subject. | +| **caveman-review** | `/caveman-review` | One-line PR comments: `L42: bug: user null. Add guard.` | +| **caveman-compress** | `/caveman-compress ` | Compress .md files to caveman prose. Saves ~46% input tokens. | +| **caveman-help** | `/caveman-help` | This card. | + +## Deactivate + +Say "stop caveman" or "normal mode". Resume anytime with `/caveman`. + +## Language + +Keep user's language by default. User write Portuguese → reply Portuguese caveman. Compress the style, not the language. Technical terms, code, commands, commit types, and exact error strings stay verbatim unless user ask for translation. + +## Configure Default Mode + +Default mode = `full`. Change it: + +**Environment variable** (highest priority): +```bash +export CAVEMAN_DEFAULT_MODE=ultra +``` + +**Config file** (`~/.config/caveman/config.json`): +```json +{ "defaultMode": "lite" } +``` + +Set `"off"` to disable auto-activation on session start. User can still activate manually with `/caveman`. + +Resolution: env var > config file > `full`. + +## More + +Full docs: https://github.com/JuliusBrussee/caveman diff --git a/.claude/skills/caveman-review/README.md b/.claude/skills/caveman-review/README.md new file mode 100644 index 0000000..acf519f --- /dev/null +++ b/.claude/skills/caveman-review/README.md @@ -0,0 +1,33 @@ +# caveman-review + +One-line PR comments. Location, problem, fix. No throat-clearing. + +## What it does + +Generates code review comments in `L: . .` format. One line per finding. Severity emoji: 🔴 bug, 🟡 risk, 🔵 nit, ❓ question. Drops "I noticed that...", hedging, and restating what the diff already shows. Keeps exact line numbers, backticked symbols, and concrete fixes. + +Auto-clarity: drops terse mode for CVE-class security findings, architectural disagreements, and onboarding contexts where the author needs the *why*. Resumes terse for the rest. + +Output only — does not approve, request changes, or run linters. + +## How to invoke + +``` +/caveman-review +``` + +Also triggers on "review this PR", "code review", "review the diff". + +## Example output + +``` +L42: 🔴 bug: user can be null after .find(). Add guard before .email. +L88-140: 🔵 nit: 50-line fn does 4 things. Extract validate/normalize/persist. +L23: 🟡 risk: no retry on 429. Wrap in withBackoff(3). +L107: ❓ q: why drop the cache here? Reads on next request will miss. +``` + +## See also + +- [`SKILL.md`](./SKILL.md) — full LLM-facing instructions +- [Caveman README](../../README.md) — repo overview diff --git a/.claude/skills/caveman-review/SKILL.md b/.claude/skills/caveman-review/SKILL.md new file mode 100644 index 0000000..48f4adb --- /dev/null +++ b/.claude/skills/caveman-review/SKILL.md @@ -0,0 +1,55 @@ +--- +name: caveman-review +description: > + Ultra-compressed code review comments. Cuts noise from PR feedback while preserving + the actionable signal. Each comment is one line: location, problem, fix. Use when user + says "review this PR", "code review", "review the diff", "/review", or invokes + /caveman-review. Auto-triggers when reviewing pull requests. +--- + +Write code review comments terse and actionable. One line per finding. Location, problem, fix. No throat-clearing. + +## Rules + +**Format:** `L: . .` — or `:L: ...` when reviewing multi-file diffs. + +**Severity prefix (optional, when mixed):** +- `🔴 bug:` — broken behavior, will cause incident +- `🟡 risk:` — works but fragile (race, missing null check, swallowed error) +- `🔵 nit:` — style, naming, micro-optim. Author can ignore +- `❓ q:` — genuine question, not a suggestion + +**Drop:** +- "I noticed that...", "It seems like...", "You might want to consider..." +- "This is just a suggestion but..." — use `nit:` instead +- "Great work!", "Looks good overall but..." — say it once at the top, not per comment +- Restating what the line does — the reviewer can read the diff +- Hedging ("perhaps", "maybe", "I think") — if unsure use `q:` + +**Keep:** +- Exact line numbers +- Exact symbol/function/variable names in backticks +- Concrete fix, not "consider refactoring this" +- The *why* if the fix isn't obvious from the problem statement + +## Examples + +❌ "I noticed that on line 42 you're not checking if the user object is null before accessing the email property. This could potentially cause a crash if the user is not found in the database. You might want to add a null check here." + +✅ `L42: 🔴 bug: user can be null after .find(). Add guard before .email.` + +❌ "It looks like this function is doing a lot of things and might benefit from being broken up into smaller functions for readability." + +✅ `L88-140: 🔵 nit: 50-line fn does 4 things. Extract validate/normalize/persist.` + +❌ "Have you considered what happens if the API returns a 429? I think we should probably handle that case." + +✅ `L23: 🟡 risk: no retry on 429. Wrap in withBackoff(3).` + +## Auto-Clarity + +Drop terse mode for: security findings (CVE-class bugs need full explanation + reference), architectural disagreements (need rationale, not just a one-liner), and onboarding contexts where the author is new and needs the "why". In those cases write a normal paragraph, then resume terse for the rest. + +## Boundaries + +Reviews only — does not write the code fix, does not approve/request-changes, does not run linters. Output the comment(s) ready to paste into the PR. "stop caveman-review" or "normal mode": revert to verbose review style. \ No newline at end of file diff --git a/.claude/skills/caveman-stats/README.md b/.claude/skills/caveman-stats/README.md new file mode 100644 index 0000000..30b28f9 --- /dev/null +++ b/.claude/skills/caveman-stats/README.md @@ -0,0 +1,30 @@ +# caveman-stats + +Real session token receipts. No AI estimation. + +## What it does + +Reads the current Claude Code session log directly and reports actual input/output token usage plus estimated savings versus a non-caveman baseline. Numbers come from the JSONL session log on disk — the model itself does not compute or estimate them. Output is injected by the `caveman-mode-tracker` hook, which intercepts `/caveman-stats` and returns the formatted stats as a blocked-decision reason. + +Each run also writes a lifetime-savings suffix file used by the statusline badge (`⛏ 12.4k`). + +## How to invoke + +``` +/caveman-stats +``` + +## Example output + +``` +Session: 47 turns +Input: 12,304 tokens +Output: 3,891 tokens (caveman) +Baseline: 11,247 tokens (estimated without caveman) +Saved: 7,356 tokens (~65%) +``` + +## See also + +- [`SKILL.md`](./SKILL.md) — hook contract and mechanics +- [Caveman README](../../README.md) — repo overview diff --git a/.claude/skills/caveman-stats/SKILL.md b/.claude/skills/caveman-stats/SKILL.md new file mode 100644 index 0000000..a7348ac --- /dev/null +++ b/.claude/skills/caveman-stats/SKILL.md @@ -0,0 +1,10 @@ +--- +name: caveman-stats +description: > + Show real token usage and estimated savings for the current session. + Reads directly from the Claude Code session log — no AI estimation. + Triggers on /caveman-stats. Output is injected by the mode-tracker hook; + the model itself does not compute the numbers. +--- + +This skill is delivered by `hooks/caveman-stats.js` (read by `hooks/caveman-mode-tracker.js` on `/caveman-stats`). The model does not need to do anything when this skill fires — the hook returns `decision: "block"` with the formatted stats as the reason. The user sees the numbers immediately. diff --git a/.claude/skills/caveman/README.md b/.claude/skills/caveman/README.md new file mode 100644 index 0000000..696a4e3 --- /dev/null +++ b/.claude/skills/caveman/README.md @@ -0,0 +1,48 @@ +# caveman + +Talk like smart caveman. Same brain, fewer tokens. + +## What it does + +Compress every model response to caveman-style prose. Drops articles, filler, pleasantries, and hedging. Keeps every technical detail, code block, error string, and symbol exact. Cuts 65% of output tokens (measured) with full accuracy preserved. Mode persists for the whole session until changed or stopped. + +Six intensity levels: + +| Level | What change | +|-------|-------------| +| `lite` | Drop filler/hedging. Sentences stay full. Professional but tight. | +| `full` | Default. Drop articles, fragments OK, short synonyms. | +| `ultra` | Bare fragments. Abbreviations (DB, auth, fn). Arrows for causality. | +| `wenyan-lite` | Classical Chinese register, light compression. | +| `wenyan-full` | Maximum 文言文. 80-90% character reduction. | +| `wenyan-ultra` | Extreme classical compression. | + +Auto-clarity rule: caveman drops to normal prose for security warnings, irreversible-action confirmations, multi-step sequences where fragment ambiguity risks misread, and when user repeats a question. Resumes after the clear part. + +## How to invoke + +``` +/caveman # full mode (default) +/caveman lite # lighter compression +/caveman ultra # extreme compression +/caveman wenyan # classical Chinese +stop caveman # back to normal prose +``` + +## Example output + +Question: "Why does my React component re-render?" + +Normal prose: +> Your component re-renders because you create a new object reference each render. Wrapping it in `useMemo` will fix the issue. + +Caveman (full): +> New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`. + +Caveman (ultra): +> Inline obj prop → new ref → re-render. `useMemo`. + +## See also + +- [`SKILL.md`](./SKILL.md) — full LLM-facing instructions +- [Caveman README](../../README.md) — repo overview, install, benchmarks diff --git a/.claude/skills/caveman/SKILL.md b/.claude/skills/caveman/SKILL.md new file mode 100644 index 0000000..adf8bc5 --- /dev/null +++ b/.claude/skills/caveman/SKILL.md @@ -0,0 +1,78 @@ +--- +name: caveman +description: > + Ultra-compressed communication mode. Cuts output tokens 65% (measured) by speaking like caveman + while keeping full technical accuracy. Supports intensity levels: lite, full (default), ultra, + wenyan-lite, wenyan-full, wenyan-ultra. + Use when user says "caveman mode", "talk like caveman", "use caveman", "less tokens", + "be brief", or invokes /caveman. Also auto-triggers when token efficiency is requested. +--- + +Respond terse like smart caveman. All technical substance stay. Only fluff die. + +## Persistence + +ACTIVE EVERY RESPONSE. No revert after many turns. No filler drift. Still active if unsure. Off only: "stop caveman" / "normal mode". + +Default: **full**. Switch: `/caveman lite|full|ultra`. + +## Rules + +Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). No tool-call narration, no decorative tables/emoji, no dumping long raw error logs unless asked — quote shortest decisive line. Standard well-known tech acronyms OK (DB/API/HTTP); never invent new abbreviations (cfg/impl/req/res/fn) — tokenizer split them same as full word: zero token saved, reader still decode. Full word cheaper AND clearer. No causal arrows (→) either — own token, save nothing. Technical terms exact. Code blocks unchanged. Errors quoted exact. + +Preserve user's dominant language. User write Portuguese → reply Portuguese caveman. User write Spanish → reply Spanish caveman. Compress the style, not the language. No forced English openings or status phrases. ALWAYS keep technical terms, code, API names, CLI commands, commit-type keywords (feat/fix/...), and exact error strings verbatim — unless user explicitly ask for translation. + +No self-reference. Never name or announce the style. No "caveman mode on", "me caveman think", no third-person caveman tags. Output caveman-only — never normal answer plus "Caveman:" recap. Exception: user explicitly ask what the mode is. + +Pattern: `[thing] [action] [reason]. [next step].` + +Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..." +Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:" + +## Intensity + +| Level | What change | +|-------|------------| +| **lite** | No filler/hedging. Keep articles + full sentences. Professional but tight | +| **full** | Drop articles, fragments OK, short synonyms. Classic caveman. No tool-call narration, no decorative tables/emoji, no long raw error-log dumps unless asked. Standard acronyms OK; no invented abbreviations | +| **ultra** | Strip conjunctions when cause-then-effect stay unambiguous. One word when one word enough. State each fact once. NO prose abbreviations (cfg/impl/req/res/fn/auth), NO arrows (X → Y) — measured zero token saving under tokenizer, cost decode clarity. Code symbols, function names, API names, error strings: never touch | +| **wenyan-lite** | Semi-classical. Drop filler/hedging but keep grammar structure, classical register | +| **wenyan-full** | Maximum classical terseness. Fully 文言文. 80-90% character reduction. Classical sentence patterns, verbs precede objects, subjects often omitted, classical particles (之/乃/為/其) | +| **wenyan-ultra** | Extreme abbreviation while keeping classical Chinese feel. Maximum compression, ultra terse | + +Example — "Why React component re-render?" +- lite: "Your component re-renders because you create a new object reference each render. Wrap it in `useMemo`." +- full: "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`." +- ultra: "Inline obj prop, new ref, re-render. `useMemo`." +- wenyan-lite: "組件頻重繪,以每繪新生對象參照故。以 useMemo 包之。" +- wenyan-full: "每繪新生對象參照,故重繪;以 useMemo 包之則免。" +- wenyan-ultra: "新參照則重繪。useMemo 包之。" + +Example — "Explain database connection pooling." +- lite: "Connection pooling reuses open connections instead of creating new ones per request. Avoids repeated handshake overhead." +- full: "Pool reuse open DB connections. No new connection per request. Skip handshake overhead." +- ultra: "Pool reuse open DB connections. No per-request handshake." +- wenyan-full: "池蓄已開之連,不逐請而新開,省握手之費。" +- wenyan-ultra: "池蓄連,免逐請新開,省握手。" + +## Auto-Clarity + +Drop caveman when: +- Security warnings +- Irreversible action confirmations +- Multi-step sequences where fragment order or omitted conjunctions risk misread +- Compression itself creates technical ambiguity (e.g., `"migrate table drop column backup first"` — order unclear without articles/conjunctions) +- User asks to clarify or repeats question + +Resume caveman after clear part done. + +Example — destructive op: +> **Warning:** This will permanently delete all rows in the `users` table and cannot be undone. +> ```sql +> DROP TABLE users; +> ``` +> Caveman resume. Verify backup exist first. + +## Boundaries + +Code/commits/PRs: write normal. "stop caveman" or "normal mode": revert. Level persist until changed or session end. \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4724e43..668fa32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,6 +3,11 @@ name: CI on: pull_request: workflow_call: + secrets: + DD_URL: + required: true + DD_ACCESS_TOKEN: + required: true jobs: test: @@ -12,10 +17,10 @@ jobs: contents: read steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: go-version-file: go.mod @@ -26,7 +31,9 @@ jobs: name: Security needs: test uses: ./.github/workflows/security.yml - secrets: inherit + secrets: + DD_URL: ${{ secrets.DD_URL }} + DD_ACCESS_TOKEN: ${{ secrets.DD_ACCESS_TOKEN }} permissions: contents: read packages: read diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index be10134..02410ef 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,9 @@ jobs: name: CI if: startsWith(github.ref, 'refs/tags/') && github.ref_type == 'tag' uses: ./.github/workflows/ci.yml - secrets: inherit + secrets: + DD_URL: ${{ secrets.DD_URL }} + DD_ACCESS_TOKEN: ${{ secrets.DD_ACCESS_TOKEN }} permissions: contents: read packages: read @@ -28,10 +30,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: registry: ghcr.io username: ${{ github.actor }} @@ -39,7 +41,7 @@ jobs: - name: Extract metadata id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5 with: images: ghcr.io/${{ github.repository }} tags: | @@ -48,7 +50,7 @@ jobs: type=ref,event=tag - name: Build and push Docker image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 with: context: . push: true diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 6191591..d555ea5 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -3,6 +3,11 @@ name: Security on: workflow_dispatch: workflow_call: + secrets: + DD_URL: + required: true + DD_ACCESS_TOKEN: + required: true jobs: security: @@ -13,10 +18,10 @@ jobs: packages: read steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: registry: ghcr.io username: ${{ github.actor }} diff --git a/README.md b/README.md index 2fa2fe5..4e34346 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,8 @@ path = "./my-service" platform = "Dockerfile" # Optional list of KICS query IDs to exclude from scanning. # exclude_queries = ["a227ec01-f97a-4084-91a4-47b350c1db54"] +# Optional list of path glob patterns to exclude from scanning. Passed to KICS via --exclude-paths. +# exclude = ["**/vendor/**", "**/node_modules/**"] # Grype – software-composition analysis (SCA) vulnerability scanner. # Enabling this section also enables Syft SBOM generation as a prerequisite. @@ -186,6 +188,7 @@ transitive_libraries = false | `path` | string | yes* | Path to the directory to scan. Resolved as `$SCAN_DIR/`. Used by both KICS and OpenGrep. | | `[kics].platform` | string | no | KICS platform filter (e.g. `Dockerfile`). When omitted KICS scans all supported types. | | `[kics].exclude_queries` | string array | no | KICS query IDs to skip (e.g. `["a227ec01-f97a-4084-91a4-47b350c1db54"]`). | +| `[kics].exclude` | string array | no | Path glob patterns to exclude from KICS scanning, passed via `--exclude-paths` (e.g. `["**/vendor/**"]`). | | `[grype].ignore_states` | string | no | Comma-separated Grype vulnerability states to suppress (e.g. `not-fixed,unknown,wont-fix`). | | `[grype].transitive_libraries` | bool | no | When `true`, Syft resolves transitive Java dependencies via Maven Central. Default: `false`. | | `[grype].exclude` | string array | no | Path glob patterns to exclude from Grype scanning (e.g. `["**/vendor/**"]`). | diff --git a/config.toml b/config.toml index 9020eaf..3dc7f1b 100644 --- a/config.toml +++ b/config.toml @@ -5,6 +5,7 @@ path = "./WebGoat" [kics] platform = "Dockerfile" +# exclude = ["**/vendor/**", "**/node_modules/**"] [grype] ignore_states = "not-fixed,unknown,wont-fix" diff --git a/features/scans/kics/const.go b/features/scans/kics/const.go index 7ee83ce..445490b 100644 --- a/features/scans/kics/const.go +++ b/features/scans/kics/const.go @@ -28,6 +28,7 @@ const ( typeArgument = "--type" ignoreOnExitArgument = "--ignore-on-exit" excludeQueriesArgument = "--exclude-queries" + excludePathsArgument = "--exclude-paths" ) const ( diff --git a/features/scans/kics/service.go b/features/scans/kics/service.go index 461e7be..357b288 100644 --- a/features/scans/kics/service.go +++ b/features/scans/kics/service.go @@ -23,6 +23,7 @@ type KicsServiceImpl struct { platform string output string excludeQueries []string + excludePaths []string proxyEnv []string } @@ -36,6 +37,7 @@ func newKicsService(path string, config loader.Kics, proxyEnv []string) interfac output: fmt.Sprintf("%s/%s/%s", environment_variable.EnvironmentVariable["SCAN_DIR"], outputFolder, outputNameParameter), platform: config.Platform, excludeQueries: config.ExcludeQueries, + excludePaths: config.Exclude, proxyEnv: proxyEnv, } } @@ -85,6 +87,10 @@ func (s *KicsServiceImpl) Start() (bool, error) { args = append(args, excludeQueriesArgument, q) } + for _, p := range s.excludePaths { + args = append(args, excludePathsArgument, p) + } + logger.Info(fmt.Sprintf(logInfoCommandLine, strings.Join(args, " "))) return exec.Wrap(binaryPath, dirPath, args, io.Discard, io.Discard, s.proxyEnv...) diff --git a/features/scans/kics/service_test.go b/features/scans/kics/service_test.go index 9bae999..18a2c6b 100644 --- a/features/scans/kics/service_test.go +++ b/features/scans/kics/service_test.go @@ -76,11 +76,12 @@ func TestNewKicsServiceImplementsInterface(t *testing.T) { } func TestNewKicsService(t *testing.T) { - service := newKicsService("./test", loader.Kics{ExcludeQueries: []string{"a227ec01-f97a-4084-91a4-47b350c1db54"}}, nil) + service := newKicsService("./test", loader.Kics{ExcludeQueries: []string{"a227ec01-f97a-4084-91a4-47b350c1db54"}, Exclude: []string{"/etc/**"}}, nil) impl, ok := service.(*KicsServiceImpl) assert.True(t, ok) assert.EqualValues(t, []string{"a227ec01-f97a-4084-91a4-47b350c1db54"}, impl.excludeQueries) + assert.EqualValues(t, []string{"/etc/**"}, impl.excludePaths) } func TestVerifyConfig(t *testing.T) { diff --git a/go.mod b/go.mod index 35d8311..0bdc0ed 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module ScopeGuardian -go 1.24.4 +go 1.25.0 require ( github.com/golang/mock v1.6.0 @@ -15,7 +15,7 @@ require ( github.com/mattn/go-runewidth v0.0.16 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect - golang.org/x/sys v0.30.0 // indirect + golang.org/x/sys v0.44.0 // indirect golang.org/x/text v0.22.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 27faf26..1c38488 100644 --- a/go.sum +++ b/go.sum @@ -31,8 +31,8 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/loader/dto.go b/loader/dto.go index 15c0acf..3b1ad5c 100644 --- a/loader/dto.go +++ b/loader/dto.go @@ -16,6 +16,9 @@ type ( Kics struct { Platform string ExcludeQueries []string `toml:"exclude_queries"` + // Exclude is a list of path glob patterns passed to KICS via --exclude-paths + // to skip matching folders/files during scanning. + Exclude []string `toml:"exclude"` } // Grype contains the configuration for the Grype vulnerability scanner. diff --git a/loader/loader_test.go b/loader/loader_test.go index 85ca483..30dc95c 100644 --- a/loader/loader_test.go +++ b/loader/loader_test.go @@ -16,6 +16,7 @@ func TestLoad(t *testing.T) { assert.NotNil(t, config.Kics) assert.EqualValues(t, "terraform", config.Kics.Platform) assert.EqualValues(t, []string{"a227ec01-f97a-4084-91a4-47b350c1db54"}, config.Kics.ExcludeQueries) + assert.EqualValues(t, []string{"/etc/**"}, config.Kics.Exclude) }) t.Run("Should load configuration file with grype section", func(t *testing.T) { diff --git a/loader/mocks/config.toml b/loader/mocks/config.toml index c583df3..130f24d 100644 --- a/loader/mocks/config.toml +++ b/loader/mocks/config.toml @@ -4,3 +4,4 @@ path = "./" [kics] platform = "terraform" exclude_queries = ["a227ec01-f97a-4084-91a4-47b350c1db54"] +exclude = ["/etc/**"]