@agenttrail/guardrails 0.2.2 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -116
- package/dist/{chunk-GNN4TGRY.js → chunk-SLJLCAZK.js} +306 -33
- package/dist/chunk-SLJLCAZK.js.map +1 -0
- package/dist/chunk-T6MJIV52.js +72 -0
- package/dist/chunk-T6MJIV52.js.map +1 -0
- package/dist/{chunk-FTTMZCDH.js → chunk-U7KZ447W.js} +65 -4
- package/dist/chunk-U7KZ447W.js.map +1 -0
- package/dist/guardrails.cjs +374 -32
- package/dist/guardrails.cjs.map +1 -1
- package/dist/guardrails.d.cts +60 -65
- package/dist/guardrails.d.ts +60 -65
- package/dist/guardrails.js +17 -1
- package/dist/index.cjs +433 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +20 -2
- package/dist/schema-B2pYoi9Z.d.cts +951 -0
- package/dist/schema-B2pYoi9Z.d.ts +951 -0
- package/dist/schema.cjs +91 -3
- package/dist/schema.cjs.map +1 -1
- package/dist/schema.d.cts +2 -892
- package/dist/schema.d.ts +2 -892
- package/dist/schema.js +4 -1
- package/package.json +2 -2
- package/dist/chunk-FTTMZCDH.js.map +0 -1
- package/dist/chunk-GNN4TGRY.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -33,6 +33,11 @@ __export(src_exports, {
|
|
|
33
33
|
LEADING_FLAGS: () => LEADING_FLAGS,
|
|
34
34
|
MatchConditionSchema: () => MatchConditionSchema,
|
|
35
35
|
MatchSchema: () => MatchSchema,
|
|
36
|
+
OWASP_AGENTIC: () => OWASP_AGENTIC,
|
|
37
|
+
OWASP_IDS: () => OWASP_IDS,
|
|
38
|
+
OWASP_LLM: () => OWASP_LLM,
|
|
39
|
+
OWASP_SOURCES: () => OWASP_SOURCES,
|
|
40
|
+
OwaspIdsSchema: () => OwaspIdsSchema,
|
|
36
41
|
PACKS: () => PACKS,
|
|
37
42
|
PRINT_MENTION: () => PRINT_MENTION,
|
|
38
43
|
QUOTED_MENTION: () => QUOTED_MENTION,
|
|
@@ -48,6 +53,7 @@ __export(src_exports, {
|
|
|
48
53
|
file: () => file,
|
|
49
54
|
getRule: () => getRule,
|
|
50
55
|
hasNestedUnboundedQuantifier: () => hasNestedUnboundedQuantifier,
|
|
56
|
+
isOwaspId: () => isOwaspId,
|
|
51
57
|
isPack: () => isPack,
|
|
52
58
|
mcp: () => mcp,
|
|
53
59
|
mentionInCommit: () => mentionInCommit,
|
|
@@ -55,6 +61,8 @@ __export(src_exports, {
|
|
|
55
61
|
mentionInPost: () => mentionInPost,
|
|
56
62
|
mentionInSearch: () => mentionInSearch,
|
|
57
63
|
mentions: () => mentions,
|
|
64
|
+
owaspIndex: () => owaspIndex,
|
|
65
|
+
owaspTitle: () => owaspTitle,
|
|
58
66
|
parseRule: () => parseRule,
|
|
59
67
|
parseRuleOrThrow: () => parseRuleOrThrow,
|
|
60
68
|
pwsh: () => pwsh,
|
|
@@ -144,6 +152,7 @@ var acAgentAutonomyFlag = {
|
|
|
144
152
|
category: "agent-context",
|
|
145
153
|
severity: "high",
|
|
146
154
|
defaultAction: "require_approval",
|
|
155
|
+
owasp: ["ASI03", "ASI10", "LLM03"],
|
|
147
156
|
title: "Switching off an agent's approvals or sandbox",
|
|
148
157
|
description: "Holds the flags that make a coding agent act without asking: Claude Code's `--dangerously-skip-permissions`, `--allow-dangerously-skip-permissions` and `--permission-mode bypassPermissions`; Codex's `--dangerously-bypass-approvals-and-sandbox`, `--yolo`, `--full-auto`, `-a never` or `--ask-for-approval never`, `--sandbox danger-full-access`, and the same two settings passed as `approval_policy` or `sandbox_mode` config values; Gemini's `--yolo`, `-y` and `--approval-mode yolo`; `cursor-agent --force`, `-f` or `--yolo`; and Aider's `--yes-always`, its accepted short form `--yes`, and `AIDER_YES_ALWAYS`. Deliberately NOT matched: the narrower modes (`--permission-mode plan` or `acceptEdits`, `--sandbox workspace-write`, `-a on-request`, `--approval-mode auto_edit`), and `--auto-approve` or `-y` on any other command \u2014 `terraform apply --auto-approve` and `apt-get install -y` are not agents. Misses Cursor's CLI under its primary name `agent`, which is too generic to match on, and a setting written to an agent's config file with a file tool, which `fs.agent-self-config` holds for Claude Code and Codex. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone, as long as every shell metacharacter stays inside the quotes.",
|
|
149
158
|
match: {
|
|
@@ -219,6 +228,7 @@ var acInstructionFileEdit = {
|
|
|
219
228
|
category: "agent-context",
|
|
220
229
|
severity: "high",
|
|
221
230
|
defaultAction: "require_approval",
|
|
231
|
+
owasp: ["ASI06", "ASI10", "LLM03"],
|
|
222
232
|
title: "The agent editing its standing instructions",
|
|
223
233
|
description: "Holds a file tool opening the instructions a coding agent loads at the start of every session: `CLAUDE.md`, `CLAUDE.local.md` and `.claude/rules/`; `AGENTS.md` and `AGENTS.override.md`, which Codex, Cursor, Windsurf, Copilot and Cline all read; `GEMINI.md`; `.cursorrules`; Windsurf's `.windsurfrules`, `.windsurf/rules/` and `.devin/rules/`; Cline's `.clinerules` file or directory and its global `Cline/Rules/` folder; Copilot's `.github/copilot-instructions.md` and `.github/instructions/**/*.instructions.md`; and `.aider.conf.yml`, which sets the files Aider reads on every launch. A line written into one of these is followed in every later session. Matched in any directory and in any letter case. Reading is excluded \u2014 the `Read` and `Grep` tools never match \u2014 so an agent opening its instructions to read them is not held; every other file tool is, including one this corpus does not know. File tools carry a path and no content, so it still cannot see what was written. Does not cover `.cursor/rules/`, which `fs.agent-self-config` holds. Misses a context file renamed through Gemini's `context.fileName` or Codex's `project_doc_fallback_filenames`, Aider's `CONVENTIONS.md`, which Aider loads only when asked and which is too common a name to match, and any of these files written by a shell command instead of a file tool.",
|
|
224
234
|
match: {
|
|
@@ -290,6 +300,7 @@ var acMcpServerAdd = {
|
|
|
290
300
|
category: "agent-context",
|
|
291
301
|
severity: "high",
|
|
292
302
|
defaultAction: "require_approval",
|
|
303
|
+
owasp: ["ASI04", "ASI10", "LLM03"],
|
|
293
304
|
title: "Adding an MCP server to an agent",
|
|
294
305
|
description: "Holds the commands that register an MCP server with a coding agent \u2014 `claude mcp add`, `claude mcp add-json`, `claude mcp add-from-claude-desktop`, `codex mcp add` and `gemini mcp add` \u2014 and `claude --mcp-config`, which attaches servers to a single session. Each gives an agent a new set of tools, often a program fetched and started on the spot, with nobody reviewing the change. Deliberately NOT matched: listing or removing servers (`claude mcp list`, `claude mcp remove`), and the MCP Inspector (`npx @modelcontextprotocol/inspector`), which is a debugging tool rather than a registration. Cursor has no command for this, so its `.cursor/mcp.json`, in a project or the home directory, is matched as a file instead \u2014 written, not read: the `Read` and `Grep` tools never match it, and every other file tool does, including one this corpus does not know; a project's `.mcp.json` is held by `fs.agent-self-config`. Misses servers written into `~/.claude.json` or Gemini's `settings.json` with a file tool, and a global flag whose value is quoted when it sits before `mcp`. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone, as long as every shell metacharacter stays inside the quotes.",
|
|
295
306
|
match: {
|
|
@@ -351,6 +362,7 @@ var acMemoryStoreEdit = {
|
|
|
351
362
|
category: "agent-context",
|
|
352
363
|
severity: "high",
|
|
353
364
|
defaultAction: "require_approval",
|
|
365
|
+
owasp: ["ASI06", "LLM03"],
|
|
354
366
|
title: "The agent editing its own memory",
|
|
355
367
|
description: "Holds a file tool opening the memory a coding agent carries between sessions: Claude Code's auto memory under `.claude/projects/<project>/memory/`, and its sub-agent memory in `.claude/agent-memory/` and `.claude/agent-memory-local/`; Codex's `.codex/memories/`; Gemini's private `.gemini/tmp/<project>/memory/`; Windsurf's `.codeium/windsurf/memories/`, including `global_rules.md`; and `.cursor/memory/`. A false fact saved here is recalled as true in every later session. A `MEMORY.md` outside those directories deliberately does NOT match, and neither does a project's own `docs/memory/` folder: the name alone is not an agent's memory. Reading is excluded \u2014 the `Read` and `Grep` tools never match \u2014 so an agent recalling a memory by reading its file is not asked; every other file tool is, including one this corpus does not know. File tools carry a path and no content, so it still cannot see what was written. Misses a memory directory moved with Claude Code's `autoMemoryDirectory` setting, and Cursor memories kept anywhere other than `.cursor/memory/`, since Cursor does not document where it stores them. Gemini memories saved into `GEMINI.md` are held by `ac.instruction-file-edit` instead.",
|
|
356
368
|
match: {
|
|
@@ -404,8 +416,9 @@ var acRecursiveAgentInvoke = {
|
|
|
404
416
|
category: "agent-context",
|
|
405
417
|
severity: "high",
|
|
406
418
|
defaultAction: "require_approval",
|
|
419
|
+
owasp: ["LLM03"],
|
|
407
420
|
title: "An agent starting another agent non-interactively",
|
|
408
|
-
description: "Holds a coding agent started from a shell to run a task on its own: `claude -p` or `--print` (including through `npx @anthropic-ai/claude-code`), `codex exec` or `codex e`, `gemini -p` or `--prompt`, `cursor-agent -p` or `--print`, and `aider` run non-interactively with `--message`, `--msg`, `-m`, or `-f` \u2014 the short form of `--message-file`, which disables chat mode. `--message-file` needs no arm of its own: the `--message` arm is a prefix of it and catches it. Every spawned agent reads, writes and runs commands of its own and can start more, which is how spend and reach multiply without anyone watching. This catches the shape, not the cost:
|
|
421
|
+
description: "Holds a coding agent started from a shell to run a task on its own: `claude -p` or `--print` (including through `npx @anthropic-ai/claude-code`), `codex exec` or `codex e`, `gemini -p` or `--prompt`, `cursor-agent -p` or `--print`, and `aider` run non-interactively with `--message`, `--msg`, `-m`, or `-f` \u2014 the short form of `--message-file`, which disables chat mode. `--message-file` needs no arm of its own: the `--message` arm is a prefix of it and catches it. Every spawned agent reads, writes and runs commands of its own and can start more, which is how spend and reach multiply without anyone watching. This catches the shape, not the cost: nothing here can count calls or tokens before a tool runs. Deliberately NOT matched: `codex -p`, which selects a profile rather than a prompt, `--version` and `--help`, and listing commands such as `claude mcp list`. Misses Cursor's CLI when it is invoked by its primary name `agent`, which is too generic to match on, Gemini run headless by piping into it without `-p`, and a flag placed after a quoted argument. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone, as long as every shell metacharacter stays inside the quotes.",
|
|
409
422
|
match: {
|
|
410
423
|
any_of: [
|
|
411
424
|
{
|
|
@@ -460,6 +473,7 @@ var acSkillInstall = {
|
|
|
460
473
|
category: "agent-context",
|
|
461
474
|
severity: "high",
|
|
462
475
|
defaultAction: "require_approval",
|
|
476
|
+
owasp: ["ASI10", "LLM03"],
|
|
463
477
|
title: "The agent installing a skill, command or sub-agent",
|
|
464
478
|
description: "Holds a file tool opening a skill, slash command, sub-agent or output style that a coding agent loads by name: Claude Code's `.claude/skills/`, `.claude/commands/`, `.claude/agents/` and `.claude/output-styles/`; the shared `.agents/skills/`; Codex's `.codex/skills/`, `.codex/prompts/` and `.codex/agents/`; Gemini's `.gemini/commands/`, `.gemini/skills/` and `.gemini/agents/`; Cursor's `.cursor/skills/`, `.cursor/agents/` and `.cursor/commands/`; Windsurf's `.windsurf/workflows/` and `.windsurf/skills/` and their global copies under `.codeium/windsurf/`; Cline's `.cline/skills/`; and a `SKILL.md` anywhere, which is how a plugin ships a skill. Each becomes a reusable instruction the agent may follow later, often with a script beside it that the agent runs. Matched at project or home level and in any letter case. Reading is excluded \u2014 the `Read` and `Grep` tools never match \u2014 so an agent opening an installed skill to read it is not held; every other file tool is, including one this corpus does not know. File tools carry a path and no content, so it still cannot see what was written. Deliberately NOT matched: an ordinary `skills/`, `commands/` or `agents/` folder in a project's source, and `.clinerules/skills/`, which `ac.instruction-file-edit` holds. Misses a skill copied into one of these folders by a shell command such as `cp` or `git clone`, and the managed system-wide workflow folders.",
|
|
465
479
|
match: {
|
|
@@ -530,8 +544,9 @@ var blockDestructiveSql = {
|
|
|
530
544
|
category: "destructive-data",
|
|
531
545
|
severity: "critical",
|
|
532
546
|
defaultAction: "block",
|
|
533
|
-
|
|
534
|
-
|
|
547
|
+
owasp: ["ASI02", "LLM03", "LLM10"],
|
|
548
|
+
title: "Block destructive SQL",
|
|
549
|
+
description: 'Blocks shell commands that execute destructive SQL DDL \u2014 DROP TABLE, TRUNCATE or DROP DATABASE. The two-word phrases are matched in any case with any spacing. Bare TRUNCATE is matched only in UPPER case, so that arm is CASE-SENSITIVE by design: lower-case `truncate` is also the coreutils binary and a common identifier, so matching it would block routine work \u2014 the cost is that a lower-case `truncate users;` without the `table` keyword is NOT caught. A command naming a read-only search or history tool (grep, rg, ag, ack, git commit/log/grep/blame/show) is left alone, because searching for the words is not executing them; a compound command that both searches and executes is therefore missed. Shell commands only: SQL issued from inside application code is invisible here. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
535
550
|
match: {
|
|
536
551
|
any_of: [
|
|
537
552
|
{
|
|
@@ -579,6 +594,7 @@ var ddAcceptDataLoss = {
|
|
|
579
594
|
category: "destructive-data",
|
|
580
595
|
severity: "critical",
|
|
581
596
|
defaultAction: "block",
|
|
597
|
+
owasp: ["ASI02", "LLM03"],
|
|
582
598
|
title: "A flag that explicitly accepts data loss",
|
|
583
599
|
description: 'Matches the flags whose own names admit the consequence \u2014 `--accept-data-loss` (Prisma), `--force-reset`, and `prisma db push --force`. A tool that makes you type those words has already decided a human should be in the loop. Does NOT match `prisma db push` without a flag, which is the ordinary spelling, and it cannot tell a scratch database from a real one because no connection string reaches the guard. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
584
600
|
match: {
|
|
@@ -617,8 +633,9 @@ var ddDatabaseDrop = {
|
|
|
617
633
|
category: "destructive-data",
|
|
618
634
|
severity: "critical",
|
|
619
635
|
defaultAction: "block",
|
|
636
|
+
owasp: ["ASI02", "LLM03"],
|
|
620
637
|
title: "Dropping a database from the command line",
|
|
621
|
-
description: 'Deletes a whole database through a shell tool rather than through SQL \u2014 `dropdb`, MongoDB\'s `dropDatabase()`, and the AWS RDS delete calls. It is the companion to block-destructive-sql, which sees the SQL statement but not `dropdb myapp`, because that command contains no DROP DATABASE phrase. Known over-match: `dropdb --help` is matched too, since the
|
|
638
|
+
description: 'Deletes a whole database through a shell tool rather than through SQL \u2014 `dropdb`, MongoDB\'s `dropDatabase()`, and the AWS RDS delete calls. It is the companion to block-destructive-sql, which sees the SQL statement but not `dropdb myapp`, because that command contains no DROP DATABASE phrase. Known over-match: `dropdb --help` is matched too, since the match reads command text and cannot tell a help flag from a target. It does NOT cover a drop issued by application code or by a migration tool (see dd.migration-reset). A global flag between `aws` and `rds` is tolerated (`aws --profile <p> rds delete-db-instance \u2026`, `--region <r>`); `dropdb` and `dropDatabase()` are single commands with no subcommand gap to exploit, and an absolute tool path still matches. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
622
639
|
match: {
|
|
623
640
|
any_of: [
|
|
624
641
|
{
|
|
@@ -659,6 +676,7 @@ var ddDockerPruneVolumes = {
|
|
|
659
676
|
category: "destructive-data",
|
|
660
677
|
severity: "high",
|
|
661
678
|
defaultAction: "require_approval",
|
|
679
|
+
owasp: ["ASI02", "LLM03"],
|
|
662
680
|
title: "Docker prune with volumes deletes unused data",
|
|
663
681
|
description: 'Prunes Docker volumes, deleting the data of every project whose containers are not currently running \u2014 on a developer laptop that is usually several other repositories\' databases. Routine housekeeping is deliberately NOT matched: `docker system prune -f` without `--volumes`, `docker image prune` and `docker builder prune` all pass. It cannot tell a volume you meant to discard from one you forgot was there. Global flags between `docker` and its subcommand are tolerated (`docker --context <name> system prune --volumes`, `-H <host>`), and an absolute tool path still matches; a flag that itself runs a program is not read. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
664
682
|
match: {
|
|
@@ -697,6 +715,7 @@ var ddDockerVolumeDestroy = {
|
|
|
697
715
|
category: "destructive-data",
|
|
698
716
|
severity: "high",
|
|
699
717
|
defaultAction: "require_approval",
|
|
718
|
+
owasp: ["ASI02", "LLM03"],
|
|
700
719
|
title: "Docker volume deletion destroys container data",
|
|
701
720
|
description: 'Deletes Docker volumes, which is where a database running in a container keeps its data \u2014 `docker compose down -v` is one character away from `docker compose down` and the character is the difference between stopping the stack and losing its contents. Does NOT match `docker compose down` without the flag, `docker ps`, or `docker volume ls`, and it cannot tell a throwaway test volume from the one holding your local development data. Global flags between `docker` and its subcommand are tolerated (`docker --context <name> compose down -v`, `-H <host>`), and an absolute tool path still matches; a flag that itself runs a program is not read. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
702
721
|
match: {
|
|
@@ -737,6 +756,7 @@ var ddMigrationReset = {
|
|
|
737
756
|
category: "destructive-data",
|
|
738
757
|
severity: "high",
|
|
739
758
|
defaultAction: "require_approval",
|
|
759
|
+
owasp: ["ASI02", "LLM03"],
|
|
740
760
|
title: "Migration reset drops and rebuilds the schema",
|
|
741
761
|
description: "Drops the schema and replays migrations from scratch \u2014 Prisma's `migrate reset`, Alembic's `downgrade base`, `rails db:reset`, Django's `flush`, Sequelize's `migrate:undo:all` and Drizzle's `drop`. Held for approval rather than blocked because it is the correct thing to do against a scratch database many times a day. The guard cannot see WHICH database is configured (no environment, no connection string reaches it), so it cannot distinguish a local reset from a production one. Does NOT match the forward commands (`migrate dev`, `migrate deploy`, `upgrade head`, `migrate:latest`). A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m \"x\" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.",
|
|
742
762
|
match: {
|
|
@@ -781,6 +801,7 @@ var ddRmRfAbsolute = {
|
|
|
781
801
|
category: "destructive-data",
|
|
782
802
|
severity: "critical",
|
|
783
803
|
defaultAction: "block",
|
|
804
|
+
owasp: ["ASI02", "LLM03"],
|
|
784
805
|
title: "rm -rf against an absolute path",
|
|
785
806
|
description: 'Recursive-force delete rooted at `/` rather than at a relative path. Deliberately does NOT fire on `rm -rf ./node_modules` or `rm -rf build`, which are safe and happen many times a day. Both the r and the f flag are required, so `rm -f /tmp/app.pid` is not blocked either. Known misses: the long forms (`rm --recursive --force /`), a quoted target (`rm -rf "/"`), and a variable target (`rm -rf $DIR`) whose value is only known at run time \u2014 for those, see require-approval-rm-rf, which holds them for approval instead. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
786
807
|
match: {
|
|
@@ -821,6 +842,7 @@ var ddShadowCopyDelete = {
|
|
|
821
842
|
category: "destructive-data",
|
|
822
843
|
severity: "critical",
|
|
823
844
|
defaultAction: "block",
|
|
845
|
+
owasp: ["ASI02", "LLM03"],
|
|
824
846
|
title: "Deleting Windows shadow copies or recovery data",
|
|
825
847
|
description: 'Deletes Windows shadow copies, the backup catalog, or the recovery boot entry \u2014 the standard opening move of ransomware, because it removes the only local route back from an encrypted disk. There is no legitimate reason for a coding agent to run any of it. Does NOT match the read-only siblings (`vssadmin list shadows`, `wbadmin get status`, `bcdedit /enum`), which are reasonable while diagnosing a backup. Windows-only by nature; the equivalent on macOS (`tmutil delete`) is not covered. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
826
848
|
match: {
|
|
@@ -873,8 +895,9 @@ var exFileUpload = {
|
|
|
873
895
|
category: "exfiltration",
|
|
874
896
|
severity: "high",
|
|
875
897
|
defaultAction: "require_approval",
|
|
898
|
+
owasp: ["ASI02", "ASI03", "LLM02"],
|
|
876
899
|
title: "Uploading a local file to a remote host",
|
|
877
|
-
description: 'Holds a command that sends a local file out: `curl -T` / `--upload-file`, `curl -F name=@file`, `curl --data-binary @file`, `curl --data @file` / `-d @file`, `wget --post-file`, and an `scp` or `rsync` whose LAST argument is a remote `host:path` (an upload). It matches only the `@`-file reference forms, so a plain `curl --data "text"` or `--data-raw`, and a `curl -F name=value` with no `@`, pass \u2014 the false positive that the `mentionInPost` helper exists to prevent. Deliberately NOT matched: `scp host:path ./local` and `rsync host:src ./dst` (downloads, remote is not last), and a local-to-local `rsync ./a/ ./b/`. MISSES `-d @-` reading from stdin, a body built by command substitution (`--data "$(cat f)"`, which runs the substitution), a colon inside a local path mistaken for a host, and an upload through an SDK or MCP tool rather than a shell command. A quoted MENTION is not a use: a search, a `git commit -m` message or an `echo` that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt. `curl --data` is NOT one of those carriers here, because
|
|
900
|
+
description: 'Holds a command that sends a local file out: `curl -T` / `--upload-file`, `curl -F name=@file`, `curl --data-binary @file`, `curl --data @file` / `-d @file`, `wget --post-file`, and an `scp` or `rsync` whose LAST argument is a remote `host:path` (an upload). It matches only the `@`-file reference forms, so a plain `curl --data "text"` or `--data-raw`, and a `curl -F name=value` with no `@`, pass \u2014 the false positive that the `mentionInPost` helper exists to prevent. Deliberately NOT matched: `scp host:path ./local` and `rsync host:src ./dst` (downloads, remote is not last), and a local-to-local `rsync ./a/ ./b/`. MISSES `-d @-` reading from stdin, a body built by command substitution (`--data "$(cat f)"`, which runs the substitution), a colon inside a local path mistaken for a host, and an upload through an SDK or MCP tool rather than a shell command. A quoted MENTION is not a use: a search, a `git commit -m` message or an `echo` that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt. `curl --data` is NOT one of those carriers here, because the trigger here names `curl`: a POST body quoting an upload still asks.',
|
|
878
901
|
match: {
|
|
879
902
|
any_of: [
|
|
880
903
|
{
|
|
@@ -927,8 +950,9 @@ var exPasteService = {
|
|
|
927
950
|
category: "exfiltration",
|
|
928
951
|
severity: "high",
|
|
929
952
|
defaultAction: "require_approval",
|
|
953
|
+
owasp: ["ASI02", "ASI03", "LLM02"],
|
|
930
954
|
title: "Posting to a public paste or file-drop service",
|
|
931
|
-
description: 'Holds a command that sends content to a public paste or file-drop service: `gh gist create`, and the services `transfer.sh`, `0x0.st`, `termbin.com`, `ix.io`, `sprunge.us`, `paste.rs`, `bashupload.com`, `file.io`, `catbox.moe`, `oshi.at`, `dpaste.com` and `pastebin.com/api`. Each returns a public URL with no account, which is the fastest way to move a secret or a dump off the machine. Matched by destination \u2014 the service name, or the `gh gist create` subcommand \u2014 because the exposure is where the content lands, not how it gets there. The two whose names collide with ordinary paths \u2014 `paste.rs` (also a Rust source filename) and `file.io` \u2014 match only in a host position: after a scheme (`//`), an `@`, or a space, and ending at a `/`, a quote, or the command, so `git add src/paste.rs` and `\u2026/file.io.json` are left alone. Deliberately NOT matched: reading a paste (`gh gist list` / `view`, `curl pastebin.com/raw/\u2026`). Some of these hosts were offline at authoring time (`transfer.sh`, `ix.io`, `sprunge.us`, `bashupload.com`, `oshi.at`); their names are kept because domains revive and can be run privately. MISSES a paste service this list does not name, a private instance on another domain, an upload made through an SDK rather than a shell command, and a `paste.rs` / `file.io` upload written with no scheme (a bare host). A quoted MENTION is not a use: a search, a `git commit -m` message or an `echo` that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt. `curl --data` is NOT one of those carriers here, because
|
|
955
|
+
description: 'Holds a command that sends content to a public paste or file-drop service: `gh gist create`, and the services `transfer.sh`, `0x0.st`, `termbin.com`, `ix.io`, `sprunge.us`, `paste.rs`, `bashupload.com`, `file.io`, `catbox.moe`, `oshi.at`, `dpaste.com` and `pastebin.com/api`. Each returns a public URL with no account, which is the fastest way to move a secret or a dump off the machine. Matched by destination \u2014 the service name, or the `gh gist create` subcommand \u2014 because the exposure is where the content lands, not how it gets there. The two whose names collide with ordinary paths \u2014 `paste.rs` (also a Rust source filename) and `file.io` \u2014 match only in a host position: after a scheme (`//`), an `@`, or a space, and ending at a `/`, a quote, or the command, so `git add src/paste.rs` and `\u2026/file.io.json` are left alone. Deliberately NOT matched: reading a paste (`gh gist list` / `view`, `curl pastebin.com/raw/\u2026`). Some of these hosts were offline at authoring time (`transfer.sh`, `ix.io`, `sprunge.us`, `bashupload.com`, `oshi.at`); their names are kept because domains revive and can be run privately. MISSES a paste service this list does not name, a private instance on another domain, an upload made through an SDK rather than a shell command, and a `paste.rs` / `file.io` upload written with no scheme (a bare host). A quoted MENTION is not a use: a search, a `git commit -m` message or an `echo` that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt. `curl --data` is NOT one of those carriers here, because the trigger here can be a `curl` upload: a POST body quoting a paste command still asks.',
|
|
932
956
|
match: {
|
|
933
957
|
any_of: [
|
|
934
958
|
{
|
|
@@ -980,8 +1004,9 @@ var exReverseShell = {
|
|
|
980
1004
|
category: "exfiltration",
|
|
981
1005
|
severity: "critical",
|
|
982
1006
|
defaultAction: "block",
|
|
1007
|
+
owasp: ["ASI02", "ASI03", "LLM02"],
|
|
983
1008
|
title: "Opening a reverse shell to a remote host",
|
|
984
|
-
description: 'Blocks a command that hands an interactive shell to another machine: a bash/zsh/ksh redirection to `/dev/tcp/` or `/dev/udp/`, `nc`/`ncat` with `-e` pointed at a shell, `ncat --exec` or `--sh-exec`, `socat` with an `EXEC:` or `SYSTEM:` address, and a `python -c` one-liner that imports `socket` together with `subprocess`, `pty.spawn` or `os.dup2`. This is the one
|
|
1009
|
+
description: 'Blocks a command that hands an interactive shell to another machine: a bash/zsh/ksh redirection to `/dev/tcp/` or `/dev/udp/`, `nc`/`ncat` with `-e` pointed at a shell, `ncat --exec` or `--sh-exec`, `socat` with an `EXEC:` or `SYSTEM:` address, and a `python -c` one-liner that imports `socket` together with `subprocess`, `pty.spawn` or `os.dup2`. This is the only one in the pack that denies rather than asks: none of these has an ordinary use in coding work. Note that plain `sh`/`dash` lack `/dev/tcp`, which is a bash/zsh/ksh feature. Deliberately NOT matched: `nc -zv host port` (a port check), `nc -l` (a listener), `socat -V`, and a `python -c` that imports `socket` alone. MISSES a `mkfifo` back-pipe shell, whose halves are split across `;`/`|` separators, a reverse shell written in Perl, Ruby, PHP or PowerShell\'s `.NET` sockets, and `nc -e` on the OpenBSD build, where `-e` means a TLS certificate name rather than a command. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
985
1010
|
match: {
|
|
986
1011
|
any_of: [
|
|
987
1012
|
{
|
|
@@ -1031,6 +1056,7 @@ var exTunnelExpose = {
|
|
|
1031
1056
|
category: "exfiltration",
|
|
1032
1057
|
severity: "high",
|
|
1033
1058
|
defaultAction: "require_approval",
|
|
1059
|
+
owasp: ["ASI02", "ASI03", "LLM02"],
|
|
1034
1060
|
title: "Exposing a local port through a public tunnel",
|
|
1035
1061
|
description: 'Holds a command that puts a local service on a public URL through a tunnel: `ngrok http|tcp|start`, `cloudflared tunnel`, `localtunnel` / `lt --port`, `tailscale funnel`, an `ssh -R` remote forward (including `-NR`, matched only when `ssh` is the command being run), the `serveo.net` and `localhost.run` SSH relays, `bore local`, `frpc` invoked with a flag (`frpc -c \u2026`), and `pinggy.io`. Each reaches past the firewall and gives the outside world a route in, which is a demo convenience and an exfiltration channel both. Deliberately NOT matched: `ssh -L` (a local forward, inbound to you) and `ssh -D` (a SOCKS proxy), `ngrok config check` / `--version`, `cloudflared --version`, `tailscale status` / `serve` (which stays inside the tailnet), `ssh-keygen -R host` (host-key removal, not a tunnel), a `-R` that appears only inside a quoted remote command (`ssh host "grep -R \u2026"`), and a path or filename that merely contains `frpc` (`scripts/frpc-parser.js`). MISSES a tunnel binary run under another name, a raw `ssh -R` to a private relay this list does not name, `frp` driven from its config file rather than the `frpc` command, `sudo frpc`, and an `frpc` subcommand invoked without a leading flag. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
1036
1062
|
match: {
|
|
@@ -1100,6 +1126,7 @@ var fsAgentSelfConfig = {
|
|
|
1100
1126
|
category: "file-scope",
|
|
1101
1127
|
severity: "high",
|
|
1102
1128
|
defaultAction: "require_approval",
|
|
1129
|
+
owasp: ["ASI02", "LLM03"],
|
|
1103
1130
|
title: "The agent editing its own configuration",
|
|
1104
1131
|
description: "Holds a file tool opening the files that define what the agent itself is allowed to do \u2014 Claude Code settings and hooks, an MCP server list, a plugin manifest, Cursor rules, a Codex config, and the guard's own config.json and guardrails.json. An agent that can edit these can widen its own reach with nobody reviewing the change. Reading is excluded \u2014 the `Read` and `Grep` tools never match \u2014 so opening one of these files to read it is not held; every other file tool is, including one this corpus does not know. Does NOT match ordinary project files, or `.claude/commands/*.md`, which are prompts rather than permissions. Bounded to WELL-KNOWN paths: no working directory or project root reaches the guard, so it can only match names it already knows.",
|
|
1105
1132
|
match: {
|
|
@@ -1143,6 +1170,7 @@ var fsCiDefinition = {
|
|
|
1143
1170
|
category: "file-scope",
|
|
1144
1171
|
severity: "medium",
|
|
1145
1172
|
defaultAction: "require_approval",
|
|
1173
|
+
owasp: ["ASI02", "LLM03"],
|
|
1146
1174
|
title: "Editing the CI pipeline definition",
|
|
1147
1175
|
description: "Holds a file tool opening a CI definition \u2014 GitHub Actions workflows and composite actions, .gitlab-ci.yml, a Jenkinsfile, CircleCI, Azure Pipelines, Buildkite or Bitbucket pipelines. This is where the checks that gate every merge are written down, and where a new step would run with the repository's secrets; both edits look like an ordinary diff. Reading is excluded \u2014 the Read and Grep tools never match \u2014 so opening a CI definition to read it is not held; every other file tool is, including one this corpus does not know. Does NOT match other files under .github/ (CODEOWNERS, issue templates), which gate nothing. It MISSES a CI system whose definition lives outside the repository.",
|
|
1148
1176
|
match: {
|
|
@@ -1186,8 +1214,9 @@ var fsSystemPaths = {
|
|
|
1186
1214
|
category: "file-scope",
|
|
1187
1215
|
severity: "high",
|
|
1188
1216
|
defaultAction: "require_approval",
|
|
1217
|
+
owasp: ["ASI02", "LLM03"],
|
|
1189
1218
|
title: "Writing to a system directory",
|
|
1190
|
-
description: "Holds a file tool writing to a path under a system directory \u2014 /etc, /bin, /sbin, /usr/bin, /usr/local/bin, /boot, /System, /Library/LaunchDaemons, or Windows/System32. Reading is excluded \u2014 the Read and Grep tools never match \u2014 so reading /etc/hosts is not held; every other file tool is, including one this corpus does not know. HONEST CEILING: this is a list of well-known ABSOLUTE paths and it cannot be anything else. No working directory and no project root reaches the guard, so the
|
|
1219
|
+
description: "Holds a file tool writing to a path under a system directory \u2014 /etc, /bin, /sbin, /usr/bin, /usr/local/bin, /boot, /System, /Library/LaunchDaemons, or Windows/System32. Reading is excluded \u2014 the Read and Grep tools never match \u2014 so reading /etc/hosts is not held; every other file tool is, including one this corpus does not know. HONEST CEILING: this is a list of well-known ABSOLUTE paths and it cannot be anything else. No working directory and no project root reaches the guard, so the check you would actually want \u2014 'the agent wrote outside the project' \u2014 is inexpressible, and would match everything or nothing. It therefore MISSES a write anywhere else outside your repository, including another project on the same machine.",
|
|
1191
1220
|
match: {
|
|
1192
1221
|
any_of: [
|
|
1193
1222
|
{ kind: "execute_tool", file_glob: "/etc/**" },
|
|
@@ -1230,8 +1259,9 @@ var fsVcsInternals = {
|
|
|
1230
1259
|
category: "file-scope",
|
|
1231
1260
|
severity: "high",
|
|
1232
1261
|
defaultAction: "require_approval",
|
|
1262
|
+
owasp: ["ASI02", "LLM03"],
|
|
1233
1263
|
title: "Editing git's internals directly",
|
|
1234
|
-
description: "Holds a file tool opening git's own bookkeeping \u2014 .git/config, .git/hooks/, .git/refs/, .git/HEAD, .git/info/exclude \u2014 where a change alters what future git commands do rather than what the repository contains. Deliberately NARROW: all of .git/** would include COMMIT_EDITMSG and the index, which change during every ordinary commit, so
|
|
1264
|
+
description: "Holds a file tool opening git's own bookkeeping \u2014 .git/config, .git/hooks/, .git/refs/, .git/HEAD, .git/info/exclude \u2014 where a change alters what future git commands do rather than what the repository contains. Deliberately NARROW: all of .git/** would include COMMIT_EDITMSG and the index, which change during every ordinary commit, so it would fire constantly and be switched off. Reading is excluded \u2014 the Read and Grep tools never match \u2014 so opening .git/config to read it is not held; every other file tool is, including one this corpus does not know. It does NOT match .gitignore, .gitattributes or anything under .github/, which are tracked project files.",
|
|
1235
1265
|
match: {
|
|
1236
1266
|
any_of: [
|
|
1237
1267
|
{ kind: "execute_tool", file_glob: "**/.git/config" },
|
|
@@ -1278,6 +1308,7 @@ var flagDependencyInstall = {
|
|
|
1278
1308
|
category: "privilege-supply-chain",
|
|
1279
1309
|
severity: "low",
|
|
1280
1310
|
defaultAction: "warn",
|
|
1311
|
+
owasp: ["ASI04", "LLM04", "LLM07"],
|
|
1281
1312
|
title: "Flag new dependency installs",
|
|
1282
1313
|
description: 'Surfaces a new third-party dependency being added \u2014 npm/pnpm/yarn/bun, pip/pipx/poetry/uv, gem, cargo, go get, composer, bundle, dotnet and mix. Non-blocking. Each form requires a PACKAGE ARGUMENT, so a lockfile restore that adds nothing \u2014 `npm ci`, `pnpm install`, `pnpm install --frozen-lockfile` \u2014 is deliberately not flagged. A global flag between the tool and its subcommand is tolerated (`npm --silent install pkg`, `pnpm -C apps/web add react`). MISSES a package named after more than one flag between the subcommand and the package, an install run through a wrapper, a dependency added by hand-editing a manifest, and the system package managers (apt, brew, apk), which install machine software rather than project dependencies. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
1283
1314
|
match: {
|
|
@@ -1337,6 +1368,7 @@ var psIamGrant = {
|
|
|
1337
1368
|
category: "privilege-supply-chain",
|
|
1338
1369
|
severity: "high",
|
|
1339
1370
|
defaultAction: "require_approval",
|
|
1371
|
+
owasp: ["ASI03", "LLM03"],
|
|
1340
1372
|
title: "Granting permissions to an identity",
|
|
1341
1373
|
description: 'Holds a command that attaches a policy, creates an access key, adds an IAM binding or creates a Kubernetes role binding. Nothing breaks at the moment it runs \u2014 the consequence is what some other identity can do afterwards, which is exactly why a person should see it. The read verbs are deliberately NOT matched (`iam list-users`, `get-iam-policy`, `get clusterrolebindings`). It cannot judge whether the grant is narrow or wide, only that one is being made. Global flags between a cloud CLI or `kubectl` and its subcommand are tolerated (`aws --profile <p> iam \u2026`, `kubectl --context <ctx> create \u2026`), and an absolute tool path still matches; a flag that itself runs a program is not read. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
1342
1374
|
match: {
|
|
@@ -1388,6 +1420,7 @@ var psPermissionWiden = {
|
|
|
1388
1420
|
category: "privilege-supply-chain",
|
|
1389
1421
|
severity: "high",
|
|
1390
1422
|
defaultAction: "require_approval",
|
|
1423
|
+
owasp: ["ASI03"],
|
|
1391
1424
|
title: "Making a file writable by everyone",
|
|
1392
1425
|
description: 'Holds a permission change that opens a file or directory to every account on the machine \u2014 `chmod 777`, `chmod a+rwx`, a setuid bit, or an icacls grant of full control to Everyone. It is the reflex fix for a permissions error and almost never the right one. The ordinary chmods that sit beside it are deliberately NOT matched: `chmod +x`, `chmod 644`, `chmod 755`. It cannot see WHAT is being widened, only that it is. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
1393
1426
|
match: {
|
|
@@ -1428,6 +1461,7 @@ var psPersistence = {
|
|
|
1428
1461
|
category: "privilege-supply-chain",
|
|
1429
1462
|
severity: "high",
|
|
1430
1463
|
defaultAction: "require_approval",
|
|
1464
|
+
owasp: ["ASI03"],
|
|
1431
1465
|
title: "Arranging to run again after the session ends",
|
|
1432
1466
|
description: 'Holds a command that installs something which outlives the session \u2014 editing a crontab, loading a launch agent, creating a scheduled task, enabling a systemd unit, appending to a shell profile, or writing a Windows Run key. Reading the same things is deliberately NOT matched (`crontab -l`, `launchctl list`, `systemctl status`, `cat ~/.zshrc`). It cannot see WHAT is being scheduled, only that something is; and it MISSES persistence installed by writing a file with a file tool rather than by a shell command. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
1433
1467
|
match: {
|
|
@@ -1472,6 +1506,7 @@ var psPublishArtifact = {
|
|
|
1472
1506
|
category: "privilege-supply-chain",
|
|
1473
1507
|
severity: "high",
|
|
1474
1508
|
defaultAction: "require_approval",
|
|
1509
|
+
owasp: ["ASI04", "LLM04"],
|
|
1475
1510
|
title: "Publishing an artifact to a public registry",
|
|
1476
1511
|
description: 'Holds a publish \u2014 npm, PyPI via twine or poetry, crates.io, RubyGems, a Docker registry, a GitHub release, or a Maven deploy. Once a version is out it is effectively permanent and other people\'s builds will fetch it, which makes this the one action in the pack whose blast radius is outside the machine. The dry runs and local builds are deliberately NOT matched (`npm pack`, `cargo package`, `docker build`, `gh release list`). It cannot tell a private registry from a public one. Global flags between `npm`/`docker` and the subcommand are tolerated (`npm --silent publish`, `docker --context <name> push \u2026`), and an absolute tool path still matches; a flag that itself runs a program is not read. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
1477
1512
|
match: {
|
|
@@ -1518,6 +1553,7 @@ var psSudoWrite = {
|
|
|
1518
1553
|
category: "privilege-supply-chain",
|
|
1519
1554
|
severity: "high",
|
|
1520
1555
|
defaultAction: "require_approval",
|
|
1556
|
+
owasp: ["ASI03"],
|
|
1521
1557
|
title: "sudo used to write or to run a shell",
|
|
1522
1558
|
description: 'Holds a `sudo` that writes or spawns a shell \u2014 `sudo tee`, `sudo dd`, `sudo cp/mv/rm/ln/install`, `sudo chown`, `sudo sh -c` \u2014 as opposed to a `sudo` that reads or queries. The read/query forms are deliberately NOT matched: `sudo apt-get update`, `sudo -l` and `sudo systemctl status` are ordinary. It reads the command\'s own words, so it MISSES a write performed by a script invoked with sudo, and it cannot tell which path is being written to. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
1523
1559
|
match: {
|
|
@@ -1569,6 +1605,7 @@ var blockProdConfigEdit = {
|
|
|
1569
1605
|
category: "prod-infra",
|
|
1570
1606
|
severity: "high",
|
|
1571
1607
|
defaultAction: "require_approval",
|
|
1608
|
+
owasp: ["ASI02", "LLM03"],
|
|
1572
1609
|
title: "Approve production config edits",
|
|
1573
1610
|
description: "Routes edits to production configuration files to human approval. Matched by path: `*.prod.*` and `*.production.*`, the bare `prod.*` / `production.*` spellings, and any file under a `prod/` or `production/` directory. Reading is excluded \u2014 the `Read` and `Grep` tools never match \u2014 so opening a production config to look at it does not ask for approval; every other file tool does, including one this corpus does not know. Prose is excluded too \u2014 `.md`, `.mdx` and `.txt` never match \u2014 so writing a runbook under `docs/production/` does not ask for approval to change production. It matches the PATH only: it cannot tell a real production config from a file that merely spells prod in its name, and it MISSES a production config named something else entirely, such as `values-live.yaml`.",
|
|
1574
1611
|
match: {
|
|
@@ -1614,8 +1651,9 @@ var piCloudResourceDelete = {
|
|
|
1614
1651
|
category: "prod-infra",
|
|
1615
1652
|
severity: "high",
|
|
1616
1653
|
defaultAction: "require_approval",
|
|
1654
|
+
owasp: ["ASI02", "LLM03"],
|
|
1617
1655
|
title: "Deleting a cloud resource from a vendor CLI",
|
|
1618
|
-
description: 'Deletes or terminates a cloud resource through the AWS, gcloud or Azure CLI, including emptying an S3 bucket. The read verbs that sit right beside them \u2014 describe, list, get \u2014 are deliberately NOT matched. This
|
|
1656
|
+
description: 'Deletes or terminates a cloud resource through the AWS, gcloud or Azure CLI, including emptying an S3 bucket. The read verbs that sit right beside them \u2014 describe, list, get \u2014 are deliberately NOT matched. This reads the command\'s own words, so it cannot tell which account or project is configured, and it MISSES a delete performed through an SDK, a Terraform apply (see pi.terraform-auto-approve), or a vendor CLI other than these three. Global flags between a cloud CLI and its subcommand are tolerated (`aws --profile <p> \u2026`, `aws --region <r> \u2026`, `gcloud --project=<p> \u2026`), and an absolute tool path still matches; a flag that itself runs a program is not read. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
1619
1657
|
match: {
|
|
1620
1658
|
any_of: [
|
|
1621
1659
|
{
|
|
@@ -1658,8 +1696,9 @@ var piDeployToProd = {
|
|
|
1658
1696
|
category: "prod-infra",
|
|
1659
1697
|
severity: "high",
|
|
1660
1698
|
defaultAction: "require_approval",
|
|
1699
|
+
owasp: ["ASI02", "LLM03"],
|
|
1661
1700
|
title: "A deploy command that names production",
|
|
1662
|
-
description: 'Ships code to a production environment through a hosting CLI whose command text says so \u2014 `vercel --prod`, `netlify deploy --prod`, `serverless deploy --stage prod`, `fly deploy`, `eb deploy`, `wrangler deploy` and Capistrano\'s production task. The preview and staging siblings are deliberately NOT matched, because a
|
|
1701
|
+
description: 'Ships code to a production environment through a hosting CLI whose command text says so \u2014 `vercel --prod`, `netlify deploy --prod`, `serverless deploy --stage prod`, `fly deploy`, `eb deploy`, `wrangler deploy` and Capistrano\'s production task. The preview and staging siblings are deliberately NOT matched, because a check that asks on every preview deploy is switched off before it ever sees a real one. It MISSES a deploy triggered by a git push, by CI, or by any script whose own text does not name the environment. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
1663
1702
|
match: {
|
|
1664
1703
|
any_of: [
|
|
1665
1704
|
{
|
|
@@ -1701,8 +1740,9 @@ var piHelmRelease = {
|
|
|
1701
1740
|
category: "prod-infra",
|
|
1702
1741
|
severity: "high",
|
|
1703
1742
|
defaultAction: "require_approval",
|
|
1743
|
+
owasp: ["ASI02", "LLM03"],
|
|
1704
1744
|
title: "Helm uninstall / rollback / forced upgrade",
|
|
1705
|
-
description: 'Removes or rewinds a Helm release, or forces an upgrade past Helm\'s own safety checks \u2014 all of which change what is running in a cluster. Does NOT match the idempotent deploy everyone actually uses (`helm upgrade --install`), nor `helm list`, `helm template` or `helm diff`. Like
|
|
1745
|
+
description: 'Removes or rewinds a Helm release, or forces an upgrade past Helm\'s own safety checks \u2014 all of which change what is running in a cluster. Does NOT match the idempotent deploy everyone actually uses (`helm upgrade --install`), nor `helm list`, `helm template` or `helm diff`. Like everything in this pack it cannot see which cluster is selected, so it treats a local kind cluster and production identically. Global flags between `helm` and its subcommand are tolerated (`helm -n <ns> uninstall \u2026`, `--kube-context <ctx>`), and an absolute tool path still matches; a flag that itself runs a program is not read. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
1706
1746
|
match: {
|
|
1707
1747
|
any_of: [
|
|
1708
1748
|
{
|
|
@@ -1741,6 +1781,7 @@ var piKubectlDelete = {
|
|
|
1741
1781
|
category: "prod-infra",
|
|
1742
1782
|
severity: "high",
|
|
1743
1783
|
defaultAction: "require_approval",
|
|
1784
|
+
owasp: ["ASI02", "LLM03"],
|
|
1744
1785
|
title: "kubectl delete / drain removes running workloads",
|
|
1745
1786
|
description: 'Deletes Kubernetes objects or drains a node, both of which stop running workloads. Held for approval rather than blocked because deleting a test deployment is routine. The guard CANNOT tell which cluster is selected \u2014 no kubeconfig, context or environment variable reaches it \u2014 so this fires the same way against a kind cluster and against production; pi.prod-namespace covers the case where the command itself names the environment. Does NOT match the read verbs (`get`, `describe`, `logs`) or `kubectl apply`. Global flags between `kubectl` and its verb are tolerated (`kubectl -n <ns> delete \u2026`, `--context <ctx>`, `--kubeconfig <f>`), and an absolute tool path still matches; a flag that itself runs a program is not read. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
1746
1787
|
match: {
|
|
@@ -1784,8 +1825,9 @@ var piProdNamespace = {
|
|
|
1784
1825
|
category: "prod-infra",
|
|
1785
1826
|
severity: "high",
|
|
1786
1827
|
defaultAction: "require_approval",
|
|
1828
|
+
owasp: ["ASI02", "LLM03"],
|
|
1787
1829
|
title: "A mutating kubectl command that names production",
|
|
1788
|
-
description: 'A kubectl command that both MUTATES (delete, apply, scale, patch, replace, rollout, drain, exec, edit, set) and names a production namespace or context in its own text. Reads are deliberately NOT matched \u2014 `kubectl get pods -n production` and `kubectl logs -n production` are how you find out what is wrong. This is the only environment signal the guard has: no kubeconfig or current-context reaches it, so a mutating command against production that does not SAY production is invisible
|
|
1830
|
+
description: 'A kubectl command that both MUTATES (delete, apply, scale, patch, replace, rollout, drain, exec, edit, set) and names a production namespace or context in its own text. Reads are deliberately NOT matched \u2014 `kubectl get pods -n production` and `kubectl logs -n production` are how you find out what is wrong. This is the only environment signal the guard has: no kubeconfig or current-context reaches it, so a mutating command against production that does not SAY production is invisible here. The mutating verb and the production namespace or context may now appear in either order and after a global flag (`kubectl -n prod delete \u2026`, `kubectl --context=prod-cluster apply \u2026`), and an absolute tool path still matches; as a consequence a config write that names production (`kubectl config set-context \u2026 --namespace prod`) is also held, which is acceptable for a prompt rather than a block. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
1789
1831
|
match: {
|
|
1790
1832
|
any_of: [
|
|
1791
1833
|
{
|
|
@@ -1823,6 +1865,7 @@ var piTerraformAutoApprove = {
|
|
|
1823
1865
|
category: "prod-infra",
|
|
1824
1866
|
severity: "high",
|
|
1825
1867
|
defaultAction: "require_approval",
|
|
1868
|
+
owasp: ["ASI02", "LLM03"],
|
|
1826
1869
|
title: "Terraform apply/destroy without the confirmation prompt",
|
|
1827
1870
|
description: 'Applies or destroys infrastructure with `-auto-approve`, which removes the interactive confirmation Terraform puts there on purpose. Held for approval rather than blocked, because it is the correct flag inside CI. Does NOT match `terraform plan`, `terraform validate`, `terraform fmt`, or `terraform apply tf.plan` against a saved plan file \u2014 a saved plan was already reviewed, which is the whole point of saving it. It cannot tell which workspace or account is selected, because no environment reaches the guard. A global option between the tool and its subcommand is tolerated (`terraform -chdir=<dir> apply -auto-approve`, `--no-color`), and an absolute tool path still matches; a flag that itself runs a program is not read. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
1828
1871
|
match: {
|
|
@@ -1860,6 +1903,7 @@ var piTerraformStateMutate = {
|
|
|
1860
1903
|
category: "prod-infra",
|
|
1861
1904
|
severity: "high",
|
|
1862
1905
|
defaultAction: "require_approval",
|
|
1906
|
+
owasp: ["ASI02", "LLM03"],
|
|
1863
1907
|
title: "Hand-editing Terraform state",
|
|
1864
1908
|
description: 'Mutates the Terraform state file directly \u2014 `state rm`, `state mv`, `state push`, `taint`, `untaint`, `force-unlock`. None of these changes any infrastructure by itself; they change what the NEXT apply believes exists, which is how a `state rm` turns into a destroyed resource two commands later. The read-only commands are deliberately NOT matched (`state list`, `state show`, `state pull`, `show`). It cannot see which backend or workspace is selected. A global option between the tool and `state`/`taint`/`force-unlock` is tolerated (`terraform -chdir=<dir> state rm \u2026`, `--no-color`), and an absolute tool path still matches; a flag that itself runs a program is not read. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
1865
1909
|
match: {
|
|
@@ -1912,8 +1956,9 @@ var blockCurlPipeToShell = {
|
|
|
1912
1956
|
category: "rce-supply-chain",
|
|
1913
1957
|
severity: "critical",
|
|
1914
1958
|
defaultAction: "block",
|
|
1959
|
+
owasp: ["ASI04", "ASI05", "LLM04"],
|
|
1915
1960
|
title: "Block curl/wget piped to a shell",
|
|
1916
|
-
description: 'Blocks a downloaded script piped straight into a shell \u2014 remote code execution \u2014 so the id and the verdict agree. Catches the shell named directly, behind a path (`| /bin/bash`), or behind sudo with its own flags (`| sudo -E bash -`) \u2014 the canonical NodeSource installer \u2014 and covers sh, bash, zsh, ksh and dash. The downloader and the pipe must be on the SAME line and in that order, with no second pipe between them. Does NOT match `curl \u2026 | jq .` or `| shasum`. MISSES the command-substitution spelling (see rce.eval-dynamic), and a download followed by a separate later execution. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt. `curl --data` is NOT one of those carriers here, because
|
|
1961
|
+
description: 'Blocks a downloaded script piped straight into a shell \u2014 remote code execution \u2014 so the id and the verdict agree. Catches the shell named directly, behind a path (`| /bin/bash`), or behind sudo with its own flags (`| sudo -E bash -`) \u2014 the canonical NodeSource installer \u2014 and covers sh, bash, zsh, ksh and dash. The downloader and the pipe must be on the SAME line and in that order, with no second pipe between them. Does NOT match `curl \u2026 | jq .` or `| shasum`. MISSES the command-substitution spelling (see rce.eval-dynamic), and a download followed by a separate later execution. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt. `curl --data` is NOT one of those carriers here, because the trigger here is a `curl`/`wget` pipeline: a POST body quoting a pipe-to-shell one-liner still blocks.',
|
|
1917
1962
|
match: {
|
|
1918
1963
|
any_of: [
|
|
1919
1964
|
{
|
|
@@ -1950,8 +1995,9 @@ var rceEvalDynamic = {
|
|
|
1950
1995
|
category: "rce-supply-chain",
|
|
1951
1996
|
severity: "high",
|
|
1952
1997
|
defaultAction: "require_approval",
|
|
1998
|
+
owasp: ["ASI04", "ASI05", "LLM04"],
|
|
1953
1999
|
title: "Executing the output of a download",
|
|
1954
|
-
description: 'Holds the command-substitution spelling of remote code execution \u2014 `bash -c "$(curl \u2026)"`, `eval "$(wget \u2026)"`, and PowerShell\'s `iex (irm \u2026)`. This is the shape block-curl-pipe-to-shell cannot see, because there is no pipe. Deliberately NOT matched: `eval` of a local command, which is how direnv, ssh-agent and every shell init line work \u2014 a
|
|
2000
|
+
description: 'Holds the command-substitution spelling of remote code execution \u2014 `bash -c "$(curl \u2026)"`, `eval "$(wget \u2026)"`, and PowerShell\'s `iex (irm \u2026)`. This is the shape block-curl-pipe-to-shell cannot see, because there is no pipe. Deliberately NOT matched: `eval` of a local command, which is how direnv, ssh-agent and every shell init line work \u2014 a check that held those would be gone by the first morning. It therefore MISSES an eval of a variable that was filled by a download two commands earlier. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt. `curl --data` is NOT one of those carriers here, because the trigger here names `curl`/`wget`; and a `$(` inside double quotes is never exempt anywhere, because the shell expands it.',
|
|
1955
2001
|
match: {
|
|
1956
2002
|
any_of: [
|
|
1957
2003
|
{
|
|
@@ -1992,6 +2038,7 @@ var rceForeignRegistry = {
|
|
|
1992
2038
|
category: "rce-supply-chain",
|
|
1993
2039
|
severity: "high",
|
|
1994
2040
|
defaultAction: "require_approval",
|
|
2041
|
+
owasp: ["ASI04", "LLM04"],
|
|
1995
2042
|
title: "Redirecting a package manager to another registry",
|
|
1996
2043
|
description: "Holds a command that points npm, yarn, pip or poetry at a registry other than the default, whether for one install or by writing the config. The guard CANNOT tell a company's own Artifactory from an attacker's mirror: it has no allow-list, and nothing in a tool call would let it be given one \u2014 so it surfaces the redirection and leaves the judgement to a person. Does NOT match reading the config (`npm config get registry`) or an ordinary install. MISSES a registry set in a committed .npmrc, which is a file edit rather than a command. Global flags between the package manager and its subcommand are tolerated (`npm --silent config set registry \u2026`, `pip --no-cache-dir install \u2026`), and an absolute tool path still matches; a flag that itself runs a program is not read. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m \"x\" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.",
|
|
1997
2044
|
match: {
|
|
@@ -2034,8 +2081,9 @@ var rceRemoteRunner = {
|
|
|
2034
2081
|
category: "rce-supply-chain",
|
|
2035
2082
|
severity: "high",
|
|
2036
2083
|
defaultAction: "require_approval",
|
|
2084
|
+
owasp: ["ASI04", "ASI05", "LLM04"],
|
|
2037
2085
|
title: "Running code straight from a URL",
|
|
2038
|
-
description: 'Holds process substitution from a downloader (`bash <(curl \u2026)`) and a package runner handed a bare URL (`npx https://\u2026`, `bunx https://\u2026`). Both execute code that was never written to disk where a human could look at it. Deliberately NOT matched: `curl \u2026 | jq .`, `curl \u2026 -o setup.sh`, and `npx --yes prettier` \u2014 downloading data and running a named package are not this. MISSES a two-step download-then-execute, where each half is ordinary on its own. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt. `curl --data` is NOT one of those carriers here, because
|
|
2086
|
+
description: 'Holds process substitution from a downloader (`bash <(curl \u2026)`) and a package runner handed a bare URL (`npx https://\u2026`, `bunx https://\u2026`). Both execute code that was never written to disk where a human could look at it. Deliberately NOT matched: `curl \u2026 | jq .`, `curl \u2026 -o setup.sh`, and `npx --yes prettier` \u2014 downloading data and running a named package are not this. MISSES a two-step download-then-execute, where each half is ordinary on its own. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt. `curl --data` is NOT one of those carriers here, because the trigger here names `curl`/`wget`: a POST body quoting one still asks.',
|
|
2039
2087
|
match: {
|
|
2040
2088
|
any_of: [
|
|
2041
2089
|
{
|
|
@@ -2074,8 +2122,9 @@ var rceTlsVerifyOff = {
|
|
|
2074
2122
|
category: "rce-supply-chain",
|
|
2075
2123
|
severity: "high",
|
|
2076
2124
|
defaultAction: "require_approval",
|
|
2125
|
+
owasp: ["ASI04", "LLM04"],
|
|
2077
2126
|
title: "Disabling TLS certificate verification",
|
|
2078
|
-
description: "Holds a command that switches off certificate verification \u2014 curl's -k/--insecure, wget's --no-check-certificate, NODE_TLS_REJECT_UNAUTHORIZED=0, git's http.sslVerify=false, pip's --trusted-host, npm's --strict-ssl=false. Each turns an encrypted channel into one anyone on the path can rewrite, which is how a dependency download becomes an arbitrary payload. Does NOT match ordinary https requests. Known over-match: any curl short-flag cluster containing the letter k is treated as -k, since the guard parses no argv. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m \"x\" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt. `curl --data` is NOT one of those carriers here, because
|
|
2127
|
+
description: "Holds a command that switches off certificate verification \u2014 curl's -k/--insecure, wget's --no-check-certificate, NODE_TLS_REJECT_UNAUTHORIZED=0, git's http.sslVerify=false, pip's --trusted-host, npm's --strict-ssl=false. Each turns an encrypted channel into one anyone on the path can rewrite, which is how a dependency download becomes an arbitrary payload. Does NOT match ordinary https requests. Known over-match: any curl short-flag cluster containing the letter k is treated as -k, since the guard parses no argv. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m \"x\" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt. `curl --data` is NOT one of those carriers here, because the trigger here IS a `curl`/`wget` flag.",
|
|
2079
2128
|
match: {
|
|
2080
2129
|
any_of: [
|
|
2081
2130
|
{
|
|
@@ -2119,6 +2168,7 @@ var rceUnverifiedPackage = {
|
|
|
2119
2168
|
category: "rce-supply-chain",
|
|
2120
2169
|
severity: "medium",
|
|
2121
2170
|
defaultAction: "require_approval",
|
|
2171
|
+
owasp: ["ASI04", "ASI05", "LLM04", "LLM07"],
|
|
2122
2172
|
title: "Installing a package from a URL or a git ref",
|
|
2123
2173
|
description: 'Holds an install whose source is a URL, a git reference or a tarball rather than a registry name \u2014 `npm i git+https://\u2026`, `pip install git+\u2026`, `cargo install --git`, `go install \u2026@main`. A registry entry is at least a name a person recognises and a version a lockfile can pin; a moving git ref is neither. Does NOT match an ordinary registry install, which is flag-dependency-install\'s job. It MISSES a git dependency declared in a manifest file rather than typed as a command. Global flags between the package manager and its subcommand are tolerated (`npm --silent i git+\u2026`, `pip --no-cache-dir install \u2026`), and an absolute tool path still matches; `cargo` and `go` are matched by their own contiguous forms, and a flag that itself runs a program is not read. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
2124
2174
|
match: {
|
|
@@ -2171,6 +2221,7 @@ var gbAdminMerge = {
|
|
|
2171
2221
|
category: "safety-bypass",
|
|
2172
2222
|
severity: "high",
|
|
2173
2223
|
defaultAction: "require_approval",
|
|
2224
|
+
owasp: ["ASI09"],
|
|
2174
2225
|
title: "Merging past branch protection, or deleting it",
|
|
2175
2226
|
description: 'Holds `gh pr merge --admin` (which merges without the required reviews or checks), a `gh api` call that deletes or replaces a branch-protection rule, and `gh ruleset delete`. Does NOT match an ordinary merge (`gh pr merge --squash`) or a READ of the protection settings, which is how you find out what is configured. It only sees the GitHub CLI: the same change made in the web UI, or through another client, is invisible to it. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
2176
2227
|
match: {
|
|
@@ -2210,6 +2261,7 @@ var gbAnsiTerminalForgery = {
|
|
|
2210
2261
|
category: "safety-bypass",
|
|
2211
2262
|
severity: "medium",
|
|
2212
2263
|
defaultAction: "warn",
|
|
2264
|
+
owasp: ["ASI09", "LLM10"],
|
|
2213
2265
|
title: "Emitting raw terminal control sequences",
|
|
2214
2266
|
description: "Warns when `echo` or `printf` emits a raw terminal control sequence \u2014 the escape introducer `\\x1b`, `\\033` or `\\e` followed by `[` (a CSI: cursor moves, screen clears, colour) or `]` (an OSC: `]0;` sets the window title, `]8;;` plants a clickable hyperlink). Rendered into a terminal, log or IDE that does not neutralise them, these forge what a human reads back. This is a broad `warn` on purpose: a colourised build line trips it too, and that is an acceptable cost for a warning. Because its own trigger is an `echo`/`printf`, the `echo` quoted-MENTION carrier is NOT exempt here \u2014 an `echo` that prints an escape is exactly the case \u2014 while a search, a `git commit -m` message and a `curl --data` body that only name one are left alone. Matches the escape written as a backslash sequence in the command text. MISSES a raw ESC byte pasted literally, a sequence printed by a compiled program or a script file rather than an inline `echo`/`printf`, and `tput`, which reads terminfo and emits nothing literal in the command. The exemption holds only while every shell metacharacter stays inside the quotes.",
|
|
2215
2267
|
match: {
|
|
@@ -2249,6 +2301,7 @@ var gbAuditTrailPurge = {
|
|
|
2249
2301
|
category: "safety-bypass",
|
|
2250
2302
|
severity: "high",
|
|
2251
2303
|
defaultAction: "require_approval",
|
|
2304
|
+
owasp: ["ASI09"],
|
|
2252
2305
|
title: "Erasing shell history or system logs",
|
|
2253
2306
|
description: 'Holds a command that erases the record of what ran: `history -c` / `-w`, `unset HISTFILE`, `HISTFILE=/dev/null`, `HISTSIZE=0`, `set +o history`, PowerShell\'s `Clear-History`; deleting or truncating a `*_history` file (`rm`, `truncate`, `shred`, `: >`); `journalctl --vacuum-time` / `--vacuum-size` / `--vacuum-files` / `--rotate`; `rm -rf`, `truncate` or `shred` against `/var/log`; and `git reflog expire --expire=now` or `git gc --prune=now`, which drop the safety net a history rewrite would otherwise leave. A READ is not matched: `history` on its own, `history | tail`, `journalctl --since`, `git reflog`, `git reflog show`, and a plain `git gc`. MISSES `HISTFILE` unset in a shell profile rather than at the prompt, a log truncated with an editor or a file tool, and a purge run by a tool other than these. A global flag between `git` and `reflog`/`gc` is tolerated (`git -C <dir> reflog expire \u2026`, `--no-pager`, `-c k=v`), and an absolute tool path such as `/usr/bin/git` still matches; a flag that itself runs a program is not read. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
2254
2307
|
match: {
|
|
@@ -2311,6 +2364,7 @@ var gbExecutionPolicyBypass = {
|
|
|
2311
2364
|
category: "safety-bypass",
|
|
2312
2365
|
severity: "high",
|
|
2313
2366
|
defaultAction: "require_approval",
|
|
2367
|
+
owasp: ["ASI09"],
|
|
2314
2368
|
title: "Bypassing the PowerShell execution policy",
|
|
2315
2369
|
description: 'Holds `Set-ExecutionPolicy Bypass/Unrestricted` and the `-ExecutionPolicy Bypass` launch flag, which together are the opening line of essentially every PowerShell-based loader. It is also occasionally what a developer legitimately needs, which is why it holds rather than blocks. Does NOT match reading the policy (`Get-ExecutionPolicy`), setting it to RemoteSigned, or an ordinary `powershell -Command`. Windows-only by nature. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
2316
2370
|
match: {
|
|
@@ -2348,8 +2402,9 @@ var gbGitNoVerify = {
|
|
|
2348
2402
|
category: "safety-bypass",
|
|
2349
2403
|
severity: "medium",
|
|
2350
2404
|
defaultAction: "require_approval",
|
|
2405
|
+
owasp: ["ASI09"],
|
|
2351
2406
|
title: "git --no-verify skips the hooks the team installed",
|
|
2352
|
-
description: 'Commits, pushes or merges with `--no-verify`, which skips the pre-commit and pre-push hooks a team installed on purpose \u2014 the formatter, the type check, the secret scanner. It is the most common way a check everyone believes is running turns out not to be. Deliberately does NOT widen to skipping tests in general: `mvn -DskipTests package` is ordinary work and is asserted as a negative. Known over-match: a `-n` anywhere in a `git commit` line, including inside a message. Global flags between `git` and its subcommand are tolerated (`git -C <dir> commit --no-verify`, `--no-pager`, `-c k=v`), and an absolute tool path such as `/usr/bin/git` still matches; a flag that itself runs a program is not read. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt. `git commit` is NOT one of those carriers here, because
|
|
2407
|
+
description: 'Commits, pushes or merges with `--no-verify`, which skips the pre-commit and pre-push hooks a team installed on purpose \u2014 the formatter, the type check, the secret scanner. It is the most common way a check everyone believes is running turns out not to be. Deliberately does NOT widen to skipping tests in general: `mvn -DskipTests package` is ordinary work and is asserted as a negative. Known over-match: a `-n` anywhere in a `git commit` line, including inside a message. Global flags between `git` and its subcommand are tolerated (`git -C <dir> commit --no-verify`, `--no-pager`, `-c k=v`), and an absolute tool path such as `/usr/bin/git` still matches; a flag that itself runs a program is not read. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt. `git commit` is NOT one of those carriers here, because the trigger here is a flag of `git commit`: a commit message that quotes `--no-verify` still asks.',
|
|
2353
2408
|
match: {
|
|
2354
2409
|
any_of: [
|
|
2355
2410
|
{
|
|
@@ -2390,6 +2445,7 @@ var gbHooksDisable = {
|
|
|
2390
2445
|
category: "safety-bypass",
|
|
2391
2446
|
severity: "medium",
|
|
2392
2447
|
defaultAction: "require_approval",
|
|
2448
|
+
owasp: ["ASI09"],
|
|
2393
2449
|
title: "Disabling git hooks at the source",
|
|
2394
2450
|
description: 'Holds a command that turns git hooks off permanently rather than for one commit \u2014 repointing `core.hooksPath`, setting HUSKY=0 or HUSKY_SKIP_HOOKS, or deleting/unsetting the executable bit on files in `.git/hooks/`. A READ of the setting (`git config core.hooksPath` with no value) is not matched. Nothing about the next commit looks any different afterwards, which is what makes it worth a prompt. Does NOT match other `git config` writes (`user.email`, `core.pager`) or installing hooks (`husky install`). It MISSES a hooksPath set through an environment variable in a shell profile. A global flag between `git` and `config` is tolerated (`git -C <dir> config \u2026`, `--no-pager`, `-c k=v`), and an absolute tool path such as `/usr/bin/git` still matches; a flag that itself runs a program is not read. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
2395
2451
|
match: {
|
|
@@ -2431,6 +2487,7 @@ var gbHostKeyBypass = {
|
|
|
2431
2487
|
category: "safety-bypass",
|
|
2432
2488
|
severity: "high",
|
|
2433
2489
|
defaultAction: "require_approval",
|
|
2490
|
+
owasp: ["ASI09"],
|
|
2434
2491
|
title: "Accepting any SSH host key",
|
|
2435
2492
|
description: 'Holds `StrictHostKeyChecking=no`, a `UserKnownHostsFile` pointed at /dev/null, or a blind `ssh-keyscan` appended to known_hosts. Host-key checking exists to notice one thing \u2014 that the machine you reached is the machine you meant \u2014 and each of these is how it stops noticing. Does NOT match ordinary ssh, git-over-ssh or key generation. It cannot tell a CI runner (where this is sometimes the pragmatic answer) from a developer laptop, because no environment reaches the guard. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
2436
2493
|
match: {
|
|
@@ -2481,6 +2538,7 @@ var blockEnvFileRead = {
|
|
|
2481
2538
|
category: "secret-exposure",
|
|
2482
2539
|
severity: "high",
|
|
2483
2540
|
defaultAction: "warn",
|
|
2541
|
+
owasp: ["ASI02", "ASI03", "LLM02"],
|
|
2484
2542
|
title: "Flag reading a .env file",
|
|
2485
2543
|
description: "Warns when a file tool READS a dotenv file \u2014 the `Read` and `Grep` tools \u2014 at both the `.env*` spelling at any depth and the `<name>.env` spelling (production.env, secrets.env). A WRITE is deliberately NOT flagged (`Edit`, `Write`, `MultiEdit`, `NotebookEdit`, and Cursor's `Delete`), because creating or editing config is ordinary work and the exposure this catches is an agent reading an existing secret, so the id names the read. A committed placeholder is NOT flagged either (.env.example, .env.sample, .env.template), because warning on a file that holds no secret teaches the reader to ignore the warning. Matches file-tool access by path: reading a .env through a shell command such as `cat .env` is a command span and is covered by se.env-print instead. It also cannot tell whether the file actually contains a secret.",
|
|
2486
2544
|
match: {
|
|
@@ -2520,8 +2578,9 @@ var blockHardcodedSecrets = {
|
|
|
2520
2578
|
category: "secret-exposure",
|
|
2521
2579
|
severity: "critical",
|
|
2522
2580
|
defaultAction: "block",
|
|
2581
|
+
owasp: ["ASI02", "ASI03", "LLM02"],
|
|
2523
2582
|
title: "Block hard-coded secrets in commands",
|
|
2524
|
-
description: "Blocks a shell command carrying a credential with the right SHAPE \u2014 an AWS access-key id (AKIA/ASIA plus 16 more characters), a GitHub token (gh?_ with a body of at least 36 characters, or github_pat_ with a long one), a Stripe live key, or PEM PRIVATE key material. Three arms are CASE-SENSITIVE and use detail_contains deliberately: these prefixes are upper- or lower-case by specification, so case IS the signal, and the length check in the same condition is what makes the difference between a mention and a key. Requiring the body is why auditing your own repo for a leak (`grep -rn AKIA .`) is not itself blocked, and requiring PRIVATE KEY beside -----BEGIN is why a public certificate is not. MISSES formats not listed (Slack, OpenAI, Google) and any secret that is not self-identifying, such as a bare password; and it sees command text only, never the contents of a file edit. A quoted MENTION is exempt on only TWO of the four carriers this corpus recognises, and this is the
|
|
2583
|
+
description: "Blocks a shell command carrying a credential with the right SHAPE \u2014 an AWS access-key id (AKIA/ASIA plus 16 more characters), a GitHub token (gh?_ with a body of at least 36 characters, or github_pat_ with a long one), a Stripe live key, or PEM PRIVATE key material. Three arms are CASE-SENSITIVE and use detail_contains deliberately: these prefixes are upper- or lower-case by specification, so case IS the signal, and the length check in the same condition is what makes the difference between a mention and a key. Requiring the body is why auditing your own repo for a leak (`grep -rn AKIA .`) is not itself blocked, and requiring PRIVATE KEY beside -----BEGIN is why a public certificate is not. MISSES formats not listed (Slack, OpenAI, Google) and any secret that is not self-identifying, such as a bare password; and it sees command text only, never the contents of a file edit. A quoted MENTION is exempt on only TWO of the four carriers this corpus recognises, and this is the only case where they are not equivalent \u2014 because here the carrier IS the exposure rather than a mention of it. A search (`grep -rn AKIA .`) and an `echo` are exempt: the key goes nowhere, and searching for one is how you find it to rotate. A `git commit -m` message and a `curl --data` body are NOT: a key in a commit message is written into history and then pushed, and a key in a POST body has already left the machine. The cost of that, stated in the other direction: documenting a REAL-looking key in a commit message is still blocked, and the only ways through are to redact the body of the key or to use a placeholder that fails the length check. Exemption also holds only while every shell metacharacter stays inside the quotes, and a single leading `sudo` aside, the carrier must be the first word.",
|
|
2525
2584
|
match: {
|
|
2526
2585
|
any_of: [
|
|
2527
2586
|
{ kind: "execute_tool", detail_contains: ["AKIA"], detail_matches: ["AKIA[0-9A-Z]{16}"] },
|
|
@@ -2537,8 +2596,8 @@ var blockHardcodedSecrets = {
|
|
|
2537
2596
|
},
|
|
2538
2597
|
{ kind: "execute_tool", detail_contains: ["-----BEGIN ", "PRIVATE KEY"] }
|
|
2539
2598
|
],
|
|
2540
|
-
// Only TWO of the four carriers are exempt here, and this is the one
|
|
2541
|
-
// that is true. For every other
|
|
2599
|
+
// Only TWO of the four carriers are exempt here, and this is the one guardrail where
|
|
2600
|
+
// that is true. For every other guardrail a carrier is inert — a commit message naming
|
|
2542
2601
|
// `rm -rf /` deletes nothing. Here the carrier IS the exposure: a live key in a
|
|
2543
2602
|
// commit message is written into history and pushed, and a live key in a POST
|
|
2544
2603
|
// body has already left the machine. Searching for a key is how you find one to
|
|
@@ -2551,7 +2610,7 @@ var blockHardcodedSecrets = {
|
|
|
2551
2610
|
bash(`export AWS_ACCESS_KEY_ID=ASIA${"EXAMPLE".repeat(3).slice(0, 16)}`),
|
|
2552
2611
|
bash(`export GH_TOKEN=ghp_${"EXAMPLE".repeat(6)}`),
|
|
2553
2612
|
bash("ssh-add - <<< '-----BEGIN OPENSSH PRIVATE KEY-----'"),
|
|
2554
|
-
// The carrier
|
|
2613
|
+
// The carrier exemption above, as fixtures. These two are `block` — MUST
|
|
2555
2614
|
// match — precisely because they are quoted mentions, which everywhere else
|
|
2556
2615
|
// in this corpus means "leave it alone".
|
|
2557
2616
|
mentionInCommit("export AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE"),
|
|
@@ -2575,6 +2634,7 @@ var requireAuthOnPiiEndpoints = {
|
|
|
2575
2634
|
category: "secret-exposure",
|
|
2576
2635
|
severity: "medium",
|
|
2577
2636
|
defaultAction: "require_approval",
|
|
2637
|
+
owasp: ["ASI02", "ASI03", "LLM02"],
|
|
2578
2638
|
title: "Review API endpoint changes for auth",
|
|
2579
2639
|
description: "Routes edits to API route/handler SOURCE files to human approval so a reviewer can confirm authentication is present on new or changed endpoints. Matched by path: a source file under routes/, handlers/ or controllers/; an api/ directory nested inside a source tree (src/api/, app/api/, pages/api/); Next's route.ts convention; and the <name>.controller.* / <name>.routes.* spellings. Reading is excluded \u2014 the Read and Grep tools never match \u2014 so opening an endpoint's source to read it does not ask for approval; every other file tool does, including one this corpus does not know. HEURISTIC: a path signal only \u2014 it cannot inspect the edit for a missing auth check or exposed PII, so treat a match as confirm auth on this endpoint, not as a finding. It deliberately does NOT match every file in a package merely NAMED api, nor a client-side router table such as routes.tsx, which defines no endpoint; and it MISSES endpoints declared inline in a server file or by a framework convention not listed above.",
|
|
2580
2640
|
match: {
|
|
@@ -2614,13 +2674,16 @@ var requireAuthOnPiiEndpoints = {
|
|
|
2614
2674
|
};
|
|
2615
2675
|
|
|
2616
2676
|
// src/packs/secret-exposure/se-credential-file.ts
|
|
2677
|
+
var NOT_VENDORED = "*(@(!(node_modules|dist|vendor)|@(node_modules|dist|vendor)[^/]*)/)";
|
|
2678
|
+
var PROD_CONFIG = "@(yml|yaml|json|toml|env|ini|conf|properties|tf|tfvars)";
|
|
2617
2679
|
var seCredentialFile = {
|
|
2618
2680
|
id: "se.credential-file",
|
|
2619
2681
|
category: "secret-exposure",
|
|
2620
2682
|
severity: "high",
|
|
2621
2683
|
defaultAction: "require_approval",
|
|
2684
|
+
owasp: ["ASI02", "ASI03", "LLM02"],
|
|
2622
2685
|
title: "Opening a file that holds credentials",
|
|
2623
|
-
description: "Holds a file tool opening a path where credentials live \u2014 an SSH private key, an AWS credentials file, a PEM/P12/PFX/JKS keystore, `.npmrc` or `.pypirc` (which hold registry tokens), a Docker config, or a kubeconfig. Public keys are excluded (`*.pub`). It matches the PATH ONLY: it cannot tell whether the file actually holds a secret, so a `.pem` that is a public certificate is held too, and a credential in a file named something else is missed entirely. `.env` files are covered separately by block-env-file-read.",
|
|
2686
|
+
description: "Holds a file tool opening a path where credentials live \u2014 an SSH private key, an AWS credentials file, a PEM/P12/PFX/JKS keystore, `.npmrc` or `.pypirc` (which hold registry tokens), a Docker config, or a kubeconfig. Public keys are excluded (`*.pub`). It matches the PATH ONLY: it cannot tell whether the file actually holds a secret, so a `.pem` that is a public certificate is held too, and a credential in a file named something else is missed entirely. `.env` files are covered separately by block-env-file-read. It also holds READING \u2014 the `Read` and `Grep` tools, nothing else \u2014 of files whose edit another guardrail already holds and which commonly carry a secret: `/etc/shadow`, `/etc/gshadow`, their `-` backups, `/etc/sudoers` and `/etc/sudoers.d/`; SSH host private keys (`/etc/ssh/ssh_host_*_key` and the legacy `/etc/ssh/ssh_host_key`); `/etc/ssl/private/` and `/etc/pki/tls/private/`; Kubernetes admin configs (`/etc/kubernetes/*.conf`) and private keys (`/etc/kubernetes/pki/**/*.key`); the k3s kubeconfig (`/etc/rancher/k3s/k3s.yaml`); a repository's `.git/config`, where a remote URL or extra header can carry a token; agent configuration that often holds API keys \u2014 `.mcp.json`, `.claude/settings.local.json`, `.claude.json`, `.cursor/mcp.json` and `.codex/config.toml`; and production configuration: a `.yml`, `.yaml`, `.json`, `.toml`, `.env`, `.ini`, `.conf`, `.properties`, `.tf` or `.tfvars` file named `*.prod.*`, `prod.*`, `*.production.*` or `production.*`, or under a `prod/` or `production/` folder, unless any folder in the path, an absolute path's leading folders included, is named exactly `node_modules`, `dist` or `vendor` (so `/srv/vendor/app/config/database.prod.yml` is not held). A Grep anywhere under `/etc`, including a search of the folder itself or with a wildcard, is held too; a Read of an `/etc` file not named here is not. Editing those files is held by the guardrail that owns the path, not by this one. NOT held on read by this guardrail: a shell command such as `cat`, on any agent (Codex has no read tool, so its reads are always shell commands); a file tool other than Read and Grep, such as Claude Code's Glob or another Cursor tool, which the guardrail that owns the path still holds under its own title, except on `.claude.json`, which no other guardrail names; `.claude/settings.json`, at project or user level; other secrets under `/etc` such as Kerberos keytabs and VPN or Wi-Fi connection files, which are held on Grep only; a Grep of a production folder with no file pattern, or with a pattern whose extension is a wildcard; production files of other types, such as `.key`, `.tfstate`, `.hcl`, `.cfg`, `.xml`, `.config` or Angular's `environment.prod.ts`; and production code or prose, such as `webpack.prod.js` or a runbook in `docs/production/`.",
|
|
2624
2687
|
match: {
|
|
2625
2688
|
any_of: [
|
|
2626
2689
|
{ kind: "execute_tool", file_glob: "**/.ssh/id_*" },
|
|
@@ -2632,7 +2695,63 @@ var seCredentialFile = {
|
|
|
2632
2695
|
{ kind: "execute_tool", file_glob: "**/.npmrc" },
|
|
2633
2696
|
{ kind: "execute_tool", file_glob: "**/.pypirc" },
|
|
2634
2697
|
{ kind: "execute_tool", file_glob: "**/.docker/config.json" },
|
|
2635
|
-
{ kind: "execute_tool", file_glob: "**/.kube/config" }
|
|
2698
|
+
{ kind: "execute_tool", file_glob: "**/.kube/config" },
|
|
2699
|
+
// Read only (see the header): system account and privilege files, host and TLS keys,
|
|
2700
|
+
// cluster admin configs and keys.
|
|
2701
|
+
{
|
|
2702
|
+
kind: "execute_tool",
|
|
2703
|
+
label: "{Read,Grep}",
|
|
2704
|
+
file_glob: "/etc/{shadow,shadow-,gshadow,gshadow-,sudoers}"
|
|
2705
|
+
},
|
|
2706
|
+
{ kind: "execute_tool", label: "{Read,Grep}", file_glob: "/etc/sudoers.d/**" },
|
|
2707
|
+
{ kind: "execute_tool", label: "{Read,Grep}", file_glob: "/etc/ssh/ssh_host_*_key" },
|
|
2708
|
+
{ kind: "execute_tool", label: "{Read,Grep}", file_glob: "/etc/ssh/ssh_host_key" },
|
|
2709
|
+
{ kind: "execute_tool", label: "{Read,Grep}", file_glob: "/etc/ssl/private/**" },
|
|
2710
|
+
{ kind: "execute_tool", label: "{Read,Grep}", file_glob: "/etc/pki/tls/private/**" },
|
|
2711
|
+
{ kind: "execute_tool", label: "{Read,Grep}", file_glob: "/etc/kubernetes/*.conf" },
|
|
2712
|
+
{ kind: "execute_tool", label: "{Read,Grep}", file_glob: "/etc/kubernetes/pki/**/*.key" },
|
|
2713
|
+
{ kind: "execute_tool", label: "{Read,Grep}", file_glob: "/etc/rancher/k3s/k3s.yaml" },
|
|
2714
|
+
// Grep only: a search anywhere under `/etc`, including the folder itself.
|
|
2715
|
+
{ kind: "execute_tool", label: "Grep", file_glob: "/etc/**" },
|
|
2716
|
+
// Read only: a remote URL or `http.extraheader` can carry a token.
|
|
2717
|
+
{ kind: "execute_tool", label: "{Read,Grep}", file_glob: "**/.git/config" },
|
|
2718
|
+
// Read only: MCP server definitions and agent settings often carry API keys in `env`.
|
|
2719
|
+
{ kind: "execute_tool", label: "{Read,Grep}", file_glob: "**/.mcp.json" },
|
|
2720
|
+
{ kind: "execute_tool", label: "{Read,Grep}", file_glob: "**/.claude/settings.local.json" },
|
|
2721
|
+
{ kind: "execute_tool", label: "{Read,Grep}", file_glob: "**/.claude.json" },
|
|
2722
|
+
{ kind: "execute_tool", label: "{Read,Grep}", file_glob: "**/.cursor/mcp.json" },
|
|
2723
|
+
{ kind: "execute_tool", label: "{Read,Grep}", file_glob: "**/.codex/config.toml" },
|
|
2724
|
+
// Read only: production configuration and secret files (see the header).
|
|
2725
|
+
{
|
|
2726
|
+
kind: "execute_tool",
|
|
2727
|
+
label: "{Read,Grep}",
|
|
2728
|
+
file_glob: `${NOT_VENDORED}*.prod.?(*.)${PROD_CONFIG}`
|
|
2729
|
+
},
|
|
2730
|
+
{
|
|
2731
|
+
kind: "execute_tool",
|
|
2732
|
+
label: "{Read,Grep}",
|
|
2733
|
+
file_glob: `${NOT_VENDORED}prod.?(*.)${PROD_CONFIG}`
|
|
2734
|
+
},
|
|
2735
|
+
{
|
|
2736
|
+
kind: "execute_tool",
|
|
2737
|
+
label: "{Read,Grep}",
|
|
2738
|
+
file_glob: `${NOT_VENDORED}*.production.?(*.)${PROD_CONFIG}`
|
|
2739
|
+
},
|
|
2740
|
+
{
|
|
2741
|
+
kind: "execute_tool",
|
|
2742
|
+
label: "{Read,Grep}",
|
|
2743
|
+
file_glob: `${NOT_VENDORED}production.?(*.)${PROD_CONFIG}`
|
|
2744
|
+
},
|
|
2745
|
+
{
|
|
2746
|
+
kind: "execute_tool",
|
|
2747
|
+
label: "{Read,Grep}",
|
|
2748
|
+
file_glob: `${NOT_VENDORED}prod/${NOT_VENDORED}*.${PROD_CONFIG}`
|
|
2749
|
+
},
|
|
2750
|
+
{
|
|
2751
|
+
kind: "execute_tool",
|
|
2752
|
+
label: "{Read,Grep}",
|
|
2753
|
+
file_glob: `${NOT_VENDORED}production/${NOT_VENDORED}*.${PROD_CONFIG}`
|
|
2754
|
+
}
|
|
2636
2755
|
],
|
|
2637
2756
|
none_of: [{ kind: "execute_tool", file_glob: "**/*.pub" }]
|
|
2638
2757
|
},
|
|
@@ -2641,13 +2760,154 @@ var seCredentialFile = {
|
|
|
2641
2760
|
file("/home/dev/.ssh/id_rsa", "Read"),
|
|
2642
2761
|
file("/Users/dev/.aws/credentials", "Read"),
|
|
2643
2762
|
file("certs/server.pem", "Read"),
|
|
2644
|
-
file(".npmrc", "Write")
|
|
2763
|
+
file(".npmrc", "Write"),
|
|
2764
|
+
// The other arms hold every tool, under a production or vendored path too: the
|
|
2765
|
+
// production arms' limits do not reach them.
|
|
2766
|
+
file("/home/prod/.ssh/id_rsa.txt", "Edit"),
|
|
2767
|
+
file("/home/prod/.ssh/id_rsa.txt", "Read"),
|
|
2768
|
+
file("/srv/production/.ssh/id_ed25519.md", "Write"),
|
|
2769
|
+
file("vendor/certs/server.pem", "Read"),
|
|
2770
|
+
// The original arms, each with a path only that arm matches.
|
|
2771
|
+
file("/home/dev/keys/id_rsa", "Read"),
|
|
2772
|
+
file("backup/id_ed25519", "Write"),
|
|
2773
|
+
file("certs/client.p12", "Read"),
|
|
2774
|
+
file("android/app/release.jks", "Edit"),
|
|
2775
|
+
file("/home/dev/.pypirc", "Read"),
|
|
2776
|
+
file("/home/dev/.docker/config.json", "Read"),
|
|
2777
|
+
file("/home/dev/.kube/config", "Grep"),
|
|
2778
|
+
// Each read-only arm, once through Read and once through Grep.
|
|
2779
|
+
file("/etc/shadow", "Read"),
|
|
2780
|
+
file("/etc/gshadow", "Grep"),
|
|
2781
|
+
file("/etc/sudoers", "Read"),
|
|
2782
|
+
file("/etc/shadow-", "Read"),
|
|
2783
|
+
file("/etc/gshadow-", "Read"),
|
|
2784
|
+
file("/etc/sudoers.d/90-cloud-init-users", "Read"),
|
|
2785
|
+
file("/etc/sudoers.d/admins", "Grep"),
|
|
2786
|
+
file("/etc/ssh/ssh_host_ed25519_key", "Read"),
|
|
2787
|
+
file("/etc/ssh/ssh_host_rsa_key", "Grep"),
|
|
2788
|
+
file("/etc/ssh/ssh_host_key", "Read"),
|
|
2789
|
+
file("/etc/ssh/ssh_host_key", "Grep"),
|
|
2790
|
+
file("/etc/ssl/private/server.key", "Read"),
|
|
2791
|
+
file("/etc/ssl/private/ca.key", "Grep"),
|
|
2792
|
+
file("/etc/pki/tls/private/server.key", "Read"),
|
|
2793
|
+
file("/etc/pki/tls/private/localhost.key", "Grep"),
|
|
2794
|
+
file("/etc/kubernetes/admin.conf", "Read"),
|
|
2795
|
+
file("/etc/kubernetes/controller-manager.conf", "Grep"),
|
|
2796
|
+
file("/etc/kubernetes/pki/ca.key", "Read"),
|
|
2797
|
+
file("/etc/kubernetes/pki/etcd/ca.key", "Grep"),
|
|
2798
|
+
file("/etc/rancher/k3s/k3s.yaml", "Read"),
|
|
2799
|
+
file("/etc/rancher/k3s/k3s.yaml", "Grep"),
|
|
2800
|
+
// The Grep-only arm: a folder, a wildcard joined to a folder, and the root of `/etc`.
|
|
2801
|
+
file("/etc", "Grep"),
|
|
2802
|
+
file("/etc/ssh", "Grep"),
|
|
2803
|
+
file("/etc/kubernetes", "Grep"),
|
|
2804
|
+
file("/etc/ssh/*_key", "Grep"),
|
|
2805
|
+
file("/etc/shadow*", "Grep"),
|
|
2806
|
+
file("/etc/hosts", "Grep"),
|
|
2807
|
+
file("/repo/.git/config", "Read"),
|
|
2808
|
+
file("/repo/.git/config", "Grep"),
|
|
2809
|
+
file(".mcp.json", "Read"),
|
|
2810
|
+
file(".mcp.json", "Grep"),
|
|
2811
|
+
file("/repo/.claude/settings.local.json", "Read"),
|
|
2812
|
+
file("/repo/.claude/settings.local.json", "Grep"),
|
|
2813
|
+
file("/Users/dev/.claude.json", "Read"),
|
|
2814
|
+
file("/Users/dev/.claude.json", "Grep"),
|
|
2815
|
+
file("/repo/.cursor/mcp.json", "Read"),
|
|
2816
|
+
file("/Users/dev/.cursor/mcp.json", "Grep"),
|
|
2817
|
+
file("/Users/dev/.codex/config.toml", "Read"),
|
|
2818
|
+
file("/Users/dev/.codex/config.toml", "Grep"),
|
|
2819
|
+
file("config/database.prod.yml", "Read"),
|
|
2820
|
+
file("docker-compose.prod.yml", "Grep"),
|
|
2821
|
+
file("config/prod.json", "Read"),
|
|
2822
|
+
file("config/prod.env", "Grep"),
|
|
2823
|
+
file("config/app.production.json", "Read"),
|
|
2824
|
+
file("config/app.production.toml", "Grep"),
|
|
2825
|
+
file("config/production.yaml", "Read"),
|
|
2826
|
+
file("config/production.env", "Grep"),
|
|
2827
|
+
file("deploy/prod/values.yaml", "Read"),
|
|
2828
|
+
file("infra/envs/prod/main.tfvars", "Grep"),
|
|
2829
|
+
file("deploy/prod/app.properties", "Read"),
|
|
2830
|
+
file("k8s/production/secrets.yaml", "Read"),
|
|
2831
|
+
file("k8s/production/configmap.yml", "Grep"),
|
|
2832
|
+
// Folders that only start with a skipped name are still held.
|
|
2833
|
+
file("distribution/prod.json", "Read"),
|
|
2834
|
+
file("vendors/config/production.yml", "Grep"),
|
|
2835
|
+
// One fixture per file type not used above.
|
|
2836
|
+
file("infra/prod/main.tf", "Read"),
|
|
2837
|
+
file("config/nginx.prod.conf", "Read"),
|
|
2838
|
+
file("config/app.prod.ini", "Grep")
|
|
2645
2839
|
],
|
|
2646
2840
|
allow: [
|
|
2647
2841
|
file("/home/dev/.ssh/id_rsa.pub", "Read"),
|
|
2648
2842
|
file("certs/server.crt", "Read"),
|
|
2649
2843
|
file("src/index.ts"),
|
|
2650
|
-
file("package.json")
|
|
2844
|
+
file("package.json"),
|
|
2845
|
+
// Near-misses of the read-only arms.
|
|
2846
|
+
file("/etc/passwd", "Read"),
|
|
2847
|
+
file("/etc/passwd-", "Read"),
|
|
2848
|
+
file("/etc/group", "Read"),
|
|
2849
|
+
file("/etc/hosts", "Read"),
|
|
2850
|
+
file("/etc/sudo.conf", "Read"),
|
|
2851
|
+
file("/etc/ssh/ssh_host_ed25519_key.pub", "Read"),
|
|
2852
|
+
file("/etc/ssh/ssh_host_key.pub", "Read"),
|
|
2853
|
+
file("/etc/ssh/sshd_config", "Read"),
|
|
2854
|
+
file("/etc/ssl/certs/ca-certificates.crt", "Read"),
|
|
2855
|
+
file("/etc/pki/tls/certs/server.crt", "Read"),
|
|
2856
|
+
file("/etc/kubernetes/manifests/kube-apiserver.yaml", "Read"),
|
|
2857
|
+
file("/etc/kubernetes/pki/ca.crt", "Read"),
|
|
2858
|
+
file("/etc/rancher/k3s/config.yaml", "Read"),
|
|
2859
|
+
file("/opt/etc/app.conf", "Grep"),
|
|
2860
|
+
file("/repo/.git/HEAD", "Read"),
|
|
2861
|
+
file("/repo/.gitconfig.example", "Read"),
|
|
2862
|
+
file("mcp.json.example", "Read"),
|
|
2863
|
+
file("/repo/.claude/settings.json", "Read"),
|
|
2864
|
+
file("/repo/claude.json", "Read"),
|
|
2865
|
+
file("/repo/.cursor/rules/style.mdc", "Read"),
|
|
2866
|
+
file("/repo/codex/config.toml", "Read"),
|
|
2867
|
+
file("config/database.staging.yml", "Read"),
|
|
2868
|
+
// Production code, prose and vendored files are not production configuration.
|
|
2869
|
+
file("node_modules/react/cjs/react.production.min.js", "Read"),
|
|
2870
|
+
file("node_modules/react-router/dist/production/index.d.ts", "Grep"),
|
|
2871
|
+
// One vendored configuration file per production arm.
|
|
2872
|
+
file("node_modules/some-lib/config/app.prod.json", "Read"),
|
|
2873
|
+
file("dist/prod.json", "Read"),
|
|
2874
|
+
file("node_modules/some-lib/config/app.production.json", "Grep"),
|
|
2875
|
+
file("vendor/config/production.yml", "Read"),
|
|
2876
|
+
file("dist/prod/app.json", "Read"),
|
|
2877
|
+
file("vendor/production/config.yml", "Grep"),
|
|
2878
|
+
file("src/Settings.prod.test.tsx", "Read"),
|
|
2879
|
+
file("infra/bin/prod.ts", "Read"),
|
|
2880
|
+
file("webpack.prod.js", "Grep"),
|
|
2881
|
+
file("config/database.prod.md", "Read"),
|
|
2882
|
+
file("config/prod.md", "Read"),
|
|
2883
|
+
file("config/app.production.mdx", "Read"),
|
|
2884
|
+
file("config/production.txt", "Read"),
|
|
2885
|
+
file("deploy/prod/RUNBOOK.txt", "Grep"),
|
|
2886
|
+
file("docs/production/README.md", "Read"),
|
|
2887
|
+
// The read-only arms do not hold an edit: the guardrail that owns each path does.
|
|
2888
|
+
file("/etc/shadow", "Edit"),
|
|
2889
|
+
file("/etc/shadow-", "Write"),
|
|
2890
|
+
file("/etc/sudoers.d/admins", "Write"),
|
|
2891
|
+
file("/etc/ssh/ssh_host_ed25519_key", "Write"),
|
|
2892
|
+
file("/etc/ssh/ssh_host_key", "Edit"),
|
|
2893
|
+
file("/etc/ssl/private/server.key", "Edit"),
|
|
2894
|
+
file("/etc/pki/tls/private/server.key", "Write"),
|
|
2895
|
+
file("/etc/kubernetes/admin.conf", "Write"),
|
|
2896
|
+
file("/etc/kubernetes/pki/sa.key", "Write"),
|
|
2897
|
+
file("/etc/rancher/k3s/k3s.yaml", "Edit"),
|
|
2898
|
+
file("/etc/hosts", "Edit"),
|
|
2899
|
+
file("/repo/.git/config", "Edit"),
|
|
2900
|
+
file(".mcp.json", "Edit"),
|
|
2901
|
+
file("/repo/.claude/settings.local.json", "Write"),
|
|
2902
|
+
file("/Users/dev/.claude.json", "Write"),
|
|
2903
|
+
file("/repo/.cursor/mcp.json", "Edit"),
|
|
2904
|
+
file("/Users/dev/.codex/config.toml", "Edit"),
|
|
2905
|
+
file("config/database.prod.yml", "Edit"),
|
|
2906
|
+
file("config/prod.json", "Write"),
|
|
2907
|
+
file("config/app.production.json", "Edit"),
|
|
2908
|
+
file("config/production.yaml", "Write"),
|
|
2909
|
+
file("deploy/prod/values.yaml", "Edit"),
|
|
2910
|
+
file("k8s/production/secrets.yaml", "Write")
|
|
2651
2911
|
]
|
|
2652
2912
|
}
|
|
2653
2913
|
};
|
|
@@ -2658,6 +2918,7 @@ var seEnvPrint = {
|
|
|
2658
2918
|
category: "secret-exposure",
|
|
2659
2919
|
severity: "medium",
|
|
2660
2920
|
defaultAction: "warn",
|
|
2921
|
+
owasp: ["ASI02", "ASI03", "LLM02"],
|
|
2661
2922
|
title: "Printing the environment or a dotenv file",
|
|
2662
2923
|
description: 'Surfaces the environment or a dotenv file being printed into the terminal, where it lands in scrollback and in the session transcript. Committed placeholder files are excluded (`.env.example`, `.env.sample`, `.env.template`). Does NOT match the POSIX `env VAR=value <command>` form, which sets a variable rather than printing one, and does NOT match a `.env` opened by a file tool \u2014 that travels the file channel and is covered by block-env-file-read. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
2663
2924
|
match: {
|
|
@@ -2707,6 +2968,7 @@ var sePublicAcl = {
|
|
|
2707
2968
|
category: "secret-exposure",
|
|
2708
2969
|
severity: "high",
|
|
2709
2970
|
defaultAction: "require_approval",
|
|
2971
|
+
owasp: ["ASI02", "ASI03", "LLM02"],
|
|
2710
2972
|
title: "Making cloud storage publicly readable",
|
|
2711
2973
|
description: 'Holds a command that opens object storage to the public \u2014 an S3 `--acl public-read`, a GCS binding to allUsers, an Azure container set to blob or container access, or turning off S3 public-access blocking. The private spellings of the same commands are deliberately NOT matched. It reads the command\'s own words, so a bucket made public through a console, a Terraform apply, or a bucket policy JSON file is invisible to it. Global flags between a storage CLI and its subcommand are tolerated (`aws --profile <p> s3api \u2026`, `gcloud --project=<p> storage \u2026`), and an absolute tool path still matches; a flag that itself runs a program is not read. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
2712
2974
|
match: {
|
|
@@ -2750,8 +3012,9 @@ var seSecretEgress = {
|
|
|
2750
3012
|
category: "secret-exposure",
|
|
2751
3013
|
severity: "high",
|
|
2752
3014
|
defaultAction: "require_approval",
|
|
3015
|
+
owasp: ["ASI02", "ASI03", "LLM02"],
|
|
2753
3016
|
title: "Sending a credential file off the machine",
|
|
2754
|
-
description: 'Holds a command that both names a credential-shaped file (.env, .pem, id_rsa, a credentials file) and hands it to a transport (curl, wget, nc, scp, rsync) in the same pipeline segment. Either half alone is ordinary, which is why both are required. It cannot read the file, so it judges by the PATH: a secret copied into a differently-named file first, or sent by application code, is invisible to it. It also does not cover an upload through a browser or an SDK. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt. `curl --data` is NOT one of those carriers here: POSTing the quoted text off the box is precisely the harm this
|
|
3017
|
+
description: 'Holds a command that both names a credential-shaped file (.env, .pem, id_rsa, a credentials file) and hands it to a transport (curl, wget, nc, scp, rsync) in the same pipeline segment. Either half alone is ordinary, which is why both are required. It cannot read the file, so it judges by the PATH: a secret copied into a differently-named file first, or sent by application code, is invisible to it. It also does not cover an upload through a browser or an SDK. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt. `curl --data` is NOT one of those carriers here: POSTing the quoted text off the box is precisely the harm this exists to catch.',
|
|
2755
3018
|
match: {
|
|
2756
3019
|
any_of: [
|
|
2757
3020
|
{
|
|
@@ -2790,6 +3053,7 @@ var seSecretManagerRead = {
|
|
|
2790
3053
|
category: "secret-exposure",
|
|
2791
3054
|
severity: "medium",
|
|
2792
3055
|
defaultAction: "warn",
|
|
3056
|
+
owasp: ["ASI02", "ASI03", "LLM02"],
|
|
2793
3057
|
title: "Reading a secret out of a secrets manager",
|
|
2794
3058
|
description: 'Surfaces a secret being read from AWS Secrets Manager or SSM, HashiCorp Vault, Google Secret Manager, Azure Key Vault, a Kubernetes secret dumped as yaml or json, or Doppler. Non-blocking: this is a normal step in a normal day, and the value is that it is visible afterwards. The listing commands are deliberately NOT matched (`list-secrets`, `vault status`, `kubectl get secrets` without an output flag). It does NOT see a secret read by application code, by an SDK, or from an environment variable already in the process. Global flags between a secrets CLI and its subcommand are tolerated (`aws --profile <p> secretsmanager \u2026`, `kubectl -n <ns> get secret \u2026`, `--region <r>`), and an absolute tool path still matches; a flag that itself runs a program is not read. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
2795
3059
|
match: {
|
|
@@ -2835,8 +3099,9 @@ var seTokenPrint = {
|
|
|
2835
3099
|
category: "secret-exposure",
|
|
2836
3100
|
severity: "medium",
|
|
2837
3101
|
defaultAction: "warn",
|
|
3102
|
+
owasp: ["ASI02", "ASI03", "LLM02"],
|
|
2838
3103
|
title: "Printing an access token into the terminal",
|
|
2839
|
-
description: 'Surfaces a command that prints a live credential \u2014 `gh auth token`, `npm token list`, an `echo` of a token-shaped variable, a `docker login` with the password on the command line, or a `.netrc` dump. Non-blocking, because seeing your own token is sometimes exactly what you need. The status siblings are deliberately NOT matched (`gh auth status`, `npm whoami`). It cannot tell whether the output is redirected, and it does NOT match a variable whose name does not contain token, secret, key or password. Global flags between `npm`/`docker` and the subcommand are tolerated (`npm --silent token list`, `docker --context <name> login \u2026`), and an absolute tool path still matches; `gh` has no global flag before its subcommand, and a flag that itself runs a program is not read. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt. `echo` is NOT one of those carriers here, because
|
|
3104
|
+
description: 'Surfaces a command that prints a live credential \u2014 `gh auth token`, `npm token list`, an `echo` of a token-shaped variable, a `docker login` with the password on the command line, or a `.netrc` dump. Non-blocking, because seeing your own token is sometimes exactly what you need. The status siblings are deliberately NOT matched (`gh auth status`, `npm whoami`). It cannot tell whether the output is redirected, and it does NOT match a variable whose name does not contain token, secret, key or password. Global flags between `npm`/`docker` and the subcommand are tolerated (`npm --silent token list`, `docker --context <name> login \u2026`), and an absolute tool path still matches; `gh` has no global flag before its subcommand, and a flag that itself runs a program is not read. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt. `echo` is NOT one of those carriers here, because the trigger here IS an `echo`: printing a quoted `$TOKEN` still warns.',
|
|
2840
3105
|
match: {
|
|
2841
3106
|
any_of: [
|
|
2842
3107
|
{
|
|
@@ -2880,6 +3145,7 @@ var warnOpReadSecret = {
|
|
|
2880
3145
|
category: "secret-exposure",
|
|
2881
3146
|
severity: "info",
|
|
2882
3147
|
defaultAction: "warn",
|
|
3148
|
+
owasp: ["ASI02", "ASI03", "LLM02"],
|
|
2883
3149
|
title: "Flag `op read` secret access",
|
|
2884
3150
|
description: 'Surfaces a secret read through the 1Password CLI (op read, op item get, op document get, op inject) so secret access gets a second look without breaking routine dev flow. Severity is `info` rather than `low` deliberately: it is warn-only over one narrow path, and `low` would overstate it. The words are matched on WORD BOUNDARIES, so a commit message containing `stop reading from cache` no longer trips it. MISSES secrets read via another CLI (aws, vault, gcloud \u2014 see se.secret-manager-read), a .env opened by a file tool, and an `op` wrapper script whose own text does not name the subcommand. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
2885
3151
|
match: {
|
|
@@ -2929,6 +3195,7 @@ var tiCiSkipMarker = {
|
|
|
2929
3195
|
category: "test-integrity",
|
|
2930
3196
|
severity: "medium",
|
|
2931
3197
|
defaultAction: "warn",
|
|
3198
|
+
owasp: ["ASI09"],
|
|
2932
3199
|
title: "Telling CI to skip a commit or push",
|
|
2933
3200
|
description: "Warns on a commit or push that tells CI not to run: a `git commit` whose message carries `[skip ci]`, `[ci skip]`, `[no ci]`, `[skip actions]`, `[actions skip]`, Azure Pipelines' `[skip azp]` family or `***NO_CI***`, or a `skip-checks: true` trailer, and `git push -o ci.skip` / `--push-option=ci.skip`. GitHub Actions, GitLab, Azure Pipelines, CircleCI and Bitbucket each honour some of these, in any letter case, so the change lands with no check run against it. Deliberately NOT matched: `skip-checks: false`, near misses such as `[ci-skip]` or `[skip deploy]`, other push options, and a commit message that only names `git push -o ci.skip`. Misses a message read from a file (`git commit -F msg.txt`), a marker added when a pull request is merged on the hosting site, and a push option set in git config. A global flag between `git` and `commit`/`push` is tolerated (`git -C <dir> commit \u2026`, `--no-pager`, `-c k=v`), and an absolute tool path such as `/usr/bin/git` still matches; a flag that itself runs a program is not read. A quoted MENTION is not a use: a search, an `echo` or a `curl --data` body that only names this command is left alone, as long as every shell metacharacter stays inside the quotes. `git commit` is NOT one of those carriers here: the marker is read from the commit message, so a message that quotes `[skip ci]` does skip CI and still warns.",
|
|
2934
3201
|
match: {
|
|
@@ -2988,6 +3255,7 @@ var tiCoverageBypass = {
|
|
|
2988
3255
|
category: "test-integrity",
|
|
2989
3256
|
severity: "medium",
|
|
2990
3257
|
defaultAction: "warn",
|
|
3258
|
+
owasp: ["ASI09"],
|
|
2991
3259
|
title: "Passing a test run with no tests or no coverage gate",
|
|
2992
3260
|
description: "Warns on a test run told to pass with nothing to check, or with its coverage gate off: `--passWithNoTests` (Jest, Vitest), pytest-cov's `--no-cov` and `--cov-fail-under=0`, `--coverage=false`, `--collectCoverage=false`, `--coverage.enabled=false` and `--no-coverage`, nyc's and c8's `--check-coverage=false` and `--no-check-coverage`, and a Vitest coverage threshold set to 0 on the command line. Each turns a check that would fail into one that reports success. A warning, not a hold: running without coverage locally is common. Deliberately NOT matched: `--no-cov-on-fail`, `--check-coverage` on its own, which makes the gate stricter, and a non-zero threshold. Misses a threshold lowered to any other number, a gate switched off in a config file (`ti.test-config-edit` holds the dedicated ones), and a coverage step removed from a CI workflow (`fs.ci-definition` holds those files). A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m \"x\" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.",
|
|
2993
3261
|
match: {
|
|
@@ -3054,6 +3322,7 @@ var tiInlineSuppressBulk = {
|
|
|
3054
3322
|
category: "test-integrity",
|
|
3055
3323
|
severity: "medium",
|
|
3056
3324
|
defaultAction: "warn",
|
|
3325
|
+
owasp: ["ASI09"],
|
|
3057
3326
|
title: "Inserting suppressions or test skips with sed or perl",
|
|
3058
3327
|
description: "Warns on an in-place `sed` or `perl` edit whose script contains a suppression or skip marker: `@ts-ignore`, `@ts-expect-error`, `@ts-nocheck`, `eslint-disable`, `# type: ignore`, `# noqa`, `# pragma: no cover`, `it.skip` / `test.skip` / `describe.skip`, `xit` / `xdescribe` / `xtest`, `@pytest.mark.skip`, Rust's `#[ignore]` and Go's `t.Skip`. One such command can silence a type error, a lint rule or a failing test across many files at once. It matches the in-place flag (`-i`, `-i.bak`, `-Ei`, `--in-place`, `perl -pi`) followed by a script naming a marker. It cannot parse the script, so it does not tell a replacement that inserts a marker from one that removes it, except the `/\u2026/d` delete-line form, which is deliberately NOT matched; neither are `sed -n` and `perl -ne`, which only print. Misses a marker added with a file tool \u2014 the Edit tool carries no content \u2014 an in-place flag written after the script, and a marker assembled from pieces. Over-matches `perl -I<dir>`, which the case-insensitive match reads as `-i`. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m \"x\" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.",
|
|
3059
3328
|
match: {
|
|
@@ -3104,6 +3373,7 @@ var tiSnapshotBlanketUpdate = {
|
|
|
3104
3373
|
category: "test-integrity",
|
|
3105
3374
|
severity: "medium",
|
|
3106
3375
|
defaultAction: "warn",
|
|
3376
|
+
owasp: ["ASI09"],
|
|
3107
3377
|
title: "Overwriting every stored snapshot with current output",
|
|
3108
3378
|
description: "Warns on a test run told to overwrite stored snapshots with whatever the code produces now: Jest's `-u` / `--updateSnapshot`, Vitest's `-u` / `--update`, Playwright's and Bun's `--update-snapshots`, the same flags passed through `npm test`, `pnpm test` or `yarn test`, pytest's `--snapshot-update` (syrupy and pytest-snapshot), `cargo insta accept`, `cargo insta test --accept`, and `INSTA_UPDATE=always`. A snapshot that no longer matches is a failing test, and accepting every difference at once makes it pass without anyone reading the diff. A warning, not a hold: regenerating snapshots after an intended change is ordinary work. Deliberately NOT matched: writing only NEW snapshots (`--update=new`, `--update-snapshots=missing`, `--snapshot-update-new-only`, `INSTA_UPDATE=new`), `cargo insta review`, and Jest's `--ci`. Misses a runner started through any other script name or wrapper, and snapshot files rewritten with a file tool. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m \"x\" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.",
|
|
3109
3379
|
match: {
|
|
@@ -3164,6 +3434,7 @@ var tiTestConfigEdit = {
|
|
|
3164
3434
|
category: "test-integrity",
|
|
3165
3435
|
severity: "medium",
|
|
3166
3436
|
defaultAction: "require_approval",
|
|
3437
|
+
owasp: ["ASI09", "ASI10"],
|
|
3167
3438
|
title: "Editing a test runner or coverage configuration",
|
|
3168
3439
|
description: "Holds a file tool opening a test runner's or coverage tool's own configuration: Jest's `jest.config.*`, Vitest's `vitest.config.*` and legacy `vitest.workspace.*`, `pytest.ini`, `pytest.toml` and their dotted forms, `tox.ini`, Mocha's `.mocharc.*`, PHPUnit's `phpunit.xml`, `phpunit.xml.dist` and `phpunit.dist.xml`, `codecov.yml`, nyc's `.nycrc*` and `nyc.config.*`, `.c8rc`, coverage.py's `.coveragerc`, `karma.conf.*`, and the Playwright and Cypress configs. One line in any of these can exclude a failing file, lower a coverage threshold or retry a flaky test until it passes. Reading is excluded \u2014 the `Read` and `Grep` tools never match \u2014 so opening one of these configs to read it is not held; every other file tool is, including one this corpus does not know. File tools carry a path and no content, so it still cannot tell a harmless edit from a weakening one. Deliberately NOT matched: general files that can also hold test settings \u2014 `pyproject.toml`, `setup.cfg`, `package.json`, `vite.config.*` \u2014 and test files themselves, since editing a test is how a test gets fixed. Misses test settings kept in those general files, and a config at a path passed with `--config`.",
|
|
3169
3440
|
match: {
|
|
@@ -3242,6 +3513,7 @@ var tiTestFileDelete = {
|
|
|
3242
3513
|
category: "test-integrity",
|
|
3243
3514
|
severity: "high",
|
|
3244
3515
|
defaultAction: "require_approval",
|
|
3516
|
+
owasp: ["ASI09", "ASI10"],
|
|
3245
3517
|
title: "Deleting a test file or test directory",
|
|
3246
3518
|
description: "Holds a command that deletes a test: `rm`, `unlink`, `git rm` or PowerShell's `Remove-Item` naming a file that follows a test runner's naming convention \u2014 `*.test.*`, `*.spec.*`, `*_test.go`, `*_test.py`, `test_*.py` \u2014 or a `__tests__/`, `test/`, `tests/` or `spec/` directory, or a file inside one. Deleting the failing test instead of fixing the code leaves a suite that still passes. Deliberately NOT matched: build, report and dependency output \u2014 anything under `dist/`, `build/`, `out/`, `coverage/` or `node_modules/`, and `test-results/`, `playwright-report/` and `.pytest_cache` \u2014 and moving or staging a test (`mv`, `git add`). Misses a test deleted with `find \u2026 -delete`, moved out of the tree, emptied or disabled with a file tool, and a test directory with any other name. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m \"x\" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.",
|
|
3247
3519
|
match: {
|
|
@@ -3305,6 +3577,7 @@ var blockForcePush = {
|
|
|
3305
3577
|
category: "working-tree",
|
|
3306
3578
|
severity: "high",
|
|
3307
3579
|
defaultAction: "block",
|
|
3580
|
+
owasp: ["ASI02", "LLM03"],
|
|
3308
3581
|
title: "Block git force-push",
|
|
3309
3582
|
description: 'Overwrites a remote branch\'s history, destroying commits other people may already have pulled. The command must contain `git push` and the force flag must sit in the same pipeline segment, so searching for the phrase is not blocked. A global flag between `git` and `push` is tolerated (`git -C <dir> push --force`, `git --no-pager push \u2026`), and an absolute tool path such as `/usr/bin/git` still matches. The safer `--force-with-lease` form IS still blocked; exempting it needs a negative lookahead this corpus does not use. MISSES an alias such as `git pf`, and a force-push issued by a wrapper script whose own text does not say `git push`. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
3310
3583
|
match: {
|
|
@@ -3345,8 +3618,9 @@ var requireApprovalRmRf = {
|
|
|
3345
3618
|
category: "working-tree",
|
|
3346
3619
|
severity: "high",
|
|
3347
3620
|
defaultAction: "require_approval",
|
|
3621
|
+
owasp: ["ASI02", "LLM03"],
|
|
3348
3622
|
title: "Hold `rm -rf` for approval",
|
|
3349
|
-
description: 'Recursive-force delete: held for approval rather than blocked, which is the honest strength for an operation that is destructive but often legitimate. Catches -rf, -Rf, -rvf and the reversed -fr spelling in any case, `rm --recursive`, and PowerShell\'s `Remove-Item -Recurse`. Clearing a build directory is EXEMPT (node_modules, dist, build, out, coverage, target, .next, .turbo, .cache, .vite, .parcel-cache): that shape appears about 220 times in real Claude Code traces against about 3 for a dangerous delete, and a
|
|
3623
|
+
description: 'Recursive-force delete: held for approval rather than blocked, which is the honest strength for an operation that is destructive but often legitimate. Catches -rf, -Rf, -rvf and the reversed -fr spelling in any case, `rm --recursive`, and PowerShell\'s `Remove-Item -Recurse`. Clearing a build directory is EXEMPT (node_modules, dist, build, out, coverage, target, .next, .turbo, .cache, .vite, .parcel-cache): that shape appears about 220 times in real Claude Code traces against about 3 for a dangerous delete, and a check that asks every time is one people switch off. An absolute path, a $VARIABLE, a ~ path or a source directory still asks. Does NOT match a delete via a file tool, or flags split across arguments such as `rm -r -f x`. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
3350
3624
|
match: {
|
|
3351
3625
|
any_of: [
|
|
3352
3626
|
{
|
|
@@ -3396,8 +3670,9 @@ var wtBranchForceDelete = {
|
|
|
3396
3670
|
category: "working-tree",
|
|
3397
3671
|
severity: "medium",
|
|
3398
3672
|
defaultAction: "require_approval",
|
|
3673
|
+
owasp: ["ASI02", "LLM03"],
|
|
3399
3674
|
title: "git branch -D force-deletes an unmerged branch",
|
|
3400
|
-
description: 'Deletes a branch even when its commits are not merged anywhere, so the work becomes unreachable. This
|
|
3675
|
+
description: 'Deletes a branch even when its commits are not merged anywhere, so the work becomes unreachable. This match is CASE-SENSITIVE on purpose, which is why it uses detail_contains: `-D` force-deletes while `-d` refuses to delete unmerged work, and the engine\'s regex matcher is case-insensitive, so a regex here would fire on the safe spelling every time a developer cleans up after a merge. Known miss: the flags written separately as `--delete --force` in the reverse order, and any alias. The `git branch` anchor tolerates repeated or tab whitespace and a global flag in between (`git branch -D \u2026`, `git -C <dir> branch -D \u2026`), and an absolute tool path such as `/usr/bin/git` still matches, while `-D` stays a case-sensitive substring so the safe `-d` is not caught. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
3401
3676
|
match: {
|
|
3402
3677
|
any_of: [
|
|
3403
3678
|
{
|
|
@@ -3440,6 +3715,7 @@ var wtCheckoutDiscard = {
|
|
|
3440
3715
|
category: "working-tree",
|
|
3441
3716
|
severity: "high",
|
|
3442
3717
|
defaultAction: "block",
|
|
3718
|
+
owasp: ["ASI02", "LLM03"],
|
|
3443
3719
|
title: "git checkout used to discard working-tree changes",
|
|
3444
3720
|
description: 'Overwrites files in the working tree from the index or from another commit, discarding uncommitted edits. Matches the discard spellings only \u2014 `git checkout -- <path>`, a bare `git checkout .`, and the `-f`/`--force` forms. It deliberately does NOT match an ordinary branch switch (`git checkout main`, `git checkout -b feature/x`), which is the same command doing something else entirely. It also MISSES `git checkout <commit> <path>` written without the `--` separator. Global flags between `git` and `checkout` are tolerated (`git -C <dir> checkout -- .`, `--no-pager`, `-c k=v`), and an absolute tool path such as `/usr/bin/git` still matches; a flag that itself runs a program is not read. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
3445
3721
|
match: {
|
|
@@ -3482,6 +3758,7 @@ var wtCleanFdx = {
|
|
|
3482
3758
|
category: "working-tree",
|
|
3483
3759
|
severity: "high",
|
|
3484
3760
|
defaultAction: "block",
|
|
3761
|
+
owasp: ["ASI02", "LLM03"],
|
|
3485
3762
|
title: "git clean -fd deletes untracked files",
|
|
3486
3763
|
description: 'Deletes untracked files and directories, and with `-x` the git-ignored ones too \u2014 which on a working checkout means local `.env` files, certificates and scratch work that exist nowhere else. Git holds no copy of any of it. The dry-run forms (`git clean -nd`, `--dry-run`) are deliberately NOT matched, since that is what a careful person runs first. Known miss: `git clean` driven from a wrapper script whose own text does not name it. Global flags between `git` and `clean` are tolerated on both the block and the dry-run exemption (`git -C <dir> clean -fd`, `--no-pager`, `-c k=v`), and an absolute tool path such as `/usr/bin/git` still matches; a flag that itself runs a program is not read. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
3487
3764
|
match: {
|
|
@@ -3531,6 +3808,7 @@ var wtResetHard = {
|
|
|
3531
3808
|
category: "working-tree",
|
|
3532
3809
|
severity: "high",
|
|
3533
3810
|
defaultAction: "block",
|
|
3811
|
+
owasp: ["ASI02", "LLM03"],
|
|
3534
3812
|
title: "git reset --hard discards uncommitted work",
|
|
3535
3813
|
description: 'Discards every uncommitted change in the working tree, irrecoverably \u2014 there is no reflog for work that was never committed. Does NOT match `git restore` (see wt.restore-path), `git checkout -- .` (see wt.checkout-discard), or a reset spelled `--hard=...`; and it cannot tell a scratch clone from your only copy of the work, so a deliberate reset in a throwaway checkout is blocked too. A global flag between `git` and `reset` is tolerated (`git -C <dir> reset --hard`, `git --no-pager \u2026`, `git -c k=v \u2026`), and an absolute tool path such as `/usr/bin/git` still matches; a flag that itself runs a program is not read. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
3536
3814
|
match: {
|
|
@@ -3572,6 +3850,7 @@ var wtResetMerge = {
|
|
|
3572
3850
|
category: "working-tree",
|
|
3573
3851
|
severity: "medium",
|
|
3574
3852
|
defaultAction: "require_approval",
|
|
3853
|
+
owasp: ["ASI02", "LLM03"],
|
|
3575
3854
|
title: "git reset --merge / --keep can discard local changes",
|
|
3576
3855
|
description: 'Resets with `--merge` or `--keep`, both of which can silently drop uncommitted changes to files that differ between HEAD and the target commit. They read as the cautious options, which is why they are worth a prompt rather than a block. Does NOT match `git reset --soft` or a bare `git reset`, neither of which touches the working tree, and it does not cover `git merge --abort`. Global flags between `git` and `reset` are tolerated (`git -C <dir> reset --merge`, `--no-pager`, `-c k=v`), and an absolute tool path such as `/usr/bin/git` still matches; a flag that itself runs a program is not read. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
3577
3856
|
match: {
|
|
@@ -3611,6 +3890,7 @@ var wtRestorePath = {
|
|
|
3611
3890
|
category: "working-tree",
|
|
3612
3891
|
severity: "high",
|
|
3613
3892
|
defaultAction: "block",
|
|
3893
|
+
owasp: ["ASI02", "LLM03"],
|
|
3614
3894
|
title: "git restore discards uncommitted changes to a path",
|
|
3615
3895
|
description: 'Overwrites files in the working tree from the index, discarding uncommitted edits to them. `git restore --staged` is deliberately NOT matched: it only unstages, and the file on disk is untouched. The cost of that exclusion is a known miss \u2014 `git restore --staged --worktree <path>` DOES discard and is exempted here, because expressing the distinction needs a negative lookahead this corpus does not use. Global flags between `git` and `restore` are tolerated on both the block and the `--staged` exemption (`git -C <dir> restore .`, `--no-pager`, `-c k=v`), and an absolute tool path such as `/usr/bin/git` still matches; a flag that itself runs a program is not read. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
3616
3896
|
match: {
|
|
@@ -3654,6 +3934,7 @@ var wtStashDrop = {
|
|
|
3654
3934
|
category: "working-tree",
|
|
3655
3935
|
severity: "medium",
|
|
3656
3936
|
defaultAction: "require_approval",
|
|
3937
|
+
owasp: ["ASI02", "LLM03"],
|
|
3657
3938
|
title: "git stash drop / clear deletes stashed work",
|
|
3658
3939
|
description: 'Deletes stashed work, which has no undo \u2014 the stash commit becomes unreachable and there is no `git stash undrop`. Held for approval rather than blocked, because clearing an old stash is a normal deliberate act. Does NOT match `git stash pop` (which applies and then drops, and whose failure mode is a conflict rather than a loss) or `git stash push`. Global flags between `git` and `stash` are tolerated (`git -C <dir> stash drop`, `--no-pager`, `-c k=v`), and an absolute tool path such as `/usr/bin/git` still matches; a flag that itself runs a program is not read. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone. That holds only while every shell metacharacter stays inside the quotes, so `git commit -m "x" && \u2026` is still caught; and the carrier must be the first word, so `sudo grep \u2026` is not exempt.',
|
|
3659
3940
|
match: {
|
|
@@ -3714,9 +3995,71 @@ function isPack(value) {
|
|
|
3714
3995
|
return PACKS.includes(value);
|
|
3715
3996
|
}
|
|
3716
3997
|
|
|
3998
|
+
// src/owasp.ts
|
|
3999
|
+
var OWASP_IDS = [
|
|
4000
|
+
"ASI01",
|
|
4001
|
+
"ASI02",
|
|
4002
|
+
"ASI03",
|
|
4003
|
+
"ASI04",
|
|
4004
|
+
"ASI05",
|
|
4005
|
+
"ASI06",
|
|
4006
|
+
"ASI07",
|
|
4007
|
+
"ASI08",
|
|
4008
|
+
"ASI09",
|
|
4009
|
+
"ASI10",
|
|
4010
|
+
"LLM01",
|
|
4011
|
+
"LLM02",
|
|
4012
|
+
"LLM03",
|
|
4013
|
+
"LLM04",
|
|
4014
|
+
"LLM05",
|
|
4015
|
+
"LLM06",
|
|
4016
|
+
"LLM07",
|
|
4017
|
+
"LLM08",
|
|
4018
|
+
"LLM09",
|
|
4019
|
+
"LLM10"
|
|
4020
|
+
];
|
|
4021
|
+
var OWASP_AGENTIC = [
|
|
4022
|
+
{ id: "ASI01", title: "Agent Goal Hijack" },
|
|
4023
|
+
{ id: "ASI02", title: "Tool Misuse and Exploitation" },
|
|
4024
|
+
{ id: "ASI03", title: "Identity and Privilege Abuse" },
|
|
4025
|
+
{ id: "ASI04", title: "Agentic Supply Chain Vulnerabilities" },
|
|
4026
|
+
{ id: "ASI05", title: "Unexpected Code Execution" },
|
|
4027
|
+
{ id: "ASI06", title: "Memory and Context Poisoning" },
|
|
4028
|
+
{ id: "ASI07", title: "Insecure Inter-Agent Communication" },
|
|
4029
|
+
{ id: "ASI08", title: "Cascading Failures" },
|
|
4030
|
+
{ id: "ASI09", title: "Human-Agent Trust Exploitation" },
|
|
4031
|
+
{ id: "ASI10", title: "Rogue Agents" }
|
|
4032
|
+
];
|
|
4033
|
+
var OWASP_LLM = [
|
|
4034
|
+
{ id: "LLM01", title: "Prompt Injection" },
|
|
4035
|
+
{ id: "LLM02", title: "Sensitive Information Disclosure" },
|
|
4036
|
+
{ id: "LLM03", title: "Excessive Agency" },
|
|
4037
|
+
{ id: "LLM04", title: "Supply Chain" },
|
|
4038
|
+
{ id: "LLM05", title: "Data and Model Poisoning" },
|
|
4039
|
+
{ id: "LLM06", title: "Unbounded Consumption" },
|
|
4040
|
+
{ id: "LLM07", title: "Misinformation" },
|
|
4041
|
+
{ id: "LLM08", title: "Hidden Context Exposure" },
|
|
4042
|
+
{ id: "LLM09", title: "Vector and Embedding Weaknesses" },
|
|
4043
|
+
{ id: "LLM10", title: "Improper Output Handling" }
|
|
4044
|
+
];
|
|
4045
|
+
var OWASP_SOURCES = {
|
|
4046
|
+
agentic: "https://genai.owasp.org/resource/owasp-top-10-for-agentic-applications-for-2026/",
|
|
4047
|
+
llm: "https://genai.owasp.org/resource/owasp-genai-llm-top-10-2026/"
|
|
4048
|
+
};
|
|
4049
|
+
function isOwaspId(value) {
|
|
4050
|
+
return typeof value === "string" && OWASP_IDS.includes(value);
|
|
4051
|
+
}
|
|
4052
|
+
function owaspIndex(id) {
|
|
4053
|
+
return OWASP_IDS.indexOf(id);
|
|
4054
|
+
}
|
|
4055
|
+
function owaspTitle(id) {
|
|
4056
|
+
const risk = [...OWASP_AGENTIC, ...OWASP_LLM].find((r) => r.id === id);
|
|
4057
|
+
return risk?.title ?? id;
|
|
4058
|
+
}
|
|
4059
|
+
|
|
3717
4060
|
// src/stamp.ts
|
|
3718
|
-
var CATALOG_VERSION = "0.2.
|
|
3719
|
-
var CATALOG_PUBLISHED_AT = "2026-10-
|
|
4061
|
+
var CATALOG_VERSION = "0.2.5";
|
|
4062
|
+
var CATALOG_PUBLISHED_AT = "2026-10-22T11:20:35Z";
|
|
3720
4063
|
|
|
3721
4064
|
// src/rules.ts
|
|
3722
4065
|
var RULES_BY_PACK = {
|
|
@@ -3857,15 +4200,70 @@ var FixturesSchema = import_zod.z.object({
|
|
|
3857
4200
|
block: import_zod.z.array(FixtureSchema).min(1),
|
|
3858
4201
|
allow: import_zod.z.array(FixtureSchema).min(1)
|
|
3859
4202
|
}).strict();
|
|
4203
|
+
function unknownOwaspIdMessage(value) {
|
|
4204
|
+
const shown = typeof value === "string" ? `\`${value}\`` : JSON.stringify(value);
|
|
4205
|
+
if (typeof value === "string") {
|
|
4206
|
+
const fixed = value.trim().toUpperCase().replace(/:\d{4}$/, "");
|
|
4207
|
+
if (isOwaspId(fixed)) {
|
|
4208
|
+
return `owasp: ${shown} is not an OWASP id. Use the upper-case id without a suffix: \`${fixed}\``;
|
|
4209
|
+
}
|
|
4210
|
+
}
|
|
4211
|
+
return `owasp: ${shown} is not an OWASP id. Use an id from the OWASP Top 10 for Agentic Applications (ASI01 to ASI10) or for LLM Applications 2026 (LLM01 to LLM10)`;
|
|
4212
|
+
}
|
|
4213
|
+
var OwaspIdsSchema = import_zod.z.array(import_zod.z.unknown()).superRefine((ids, ctx) => {
|
|
4214
|
+
if (ids.length === 0) {
|
|
4215
|
+
ctx.addIssue({
|
|
4216
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
4217
|
+
message: "owasp: omit the field rather than giving an empty list"
|
|
4218
|
+
});
|
|
4219
|
+
return;
|
|
4220
|
+
}
|
|
4221
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4222
|
+
let clean = true;
|
|
4223
|
+
ids.forEach((id, i) => {
|
|
4224
|
+
if (!isOwaspId(id)) {
|
|
4225
|
+
clean = false;
|
|
4226
|
+
ctx.addIssue({
|
|
4227
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
4228
|
+
path: [i],
|
|
4229
|
+
message: unknownOwaspIdMessage(id)
|
|
4230
|
+
});
|
|
4231
|
+
return;
|
|
4232
|
+
}
|
|
4233
|
+
if (seen.has(id)) {
|
|
4234
|
+
clean = false;
|
|
4235
|
+
ctx.addIssue({
|
|
4236
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
4237
|
+
path: [i],
|
|
4238
|
+
message: `owasp: \`${id}\` is listed twice`
|
|
4239
|
+
});
|
|
4240
|
+
}
|
|
4241
|
+
seen.add(id);
|
|
4242
|
+
});
|
|
4243
|
+
if (!clean) return;
|
|
4244
|
+
const indexes = ids.map(owaspIndex);
|
|
4245
|
+
if (indexes.some((at, i) => i > 0 && at < (indexes[i - 1] ?? at))) {
|
|
4246
|
+
ctx.addIssue({
|
|
4247
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
4248
|
+
message: "owasp: out of order; list ASI ids before LLM ids, each ascending"
|
|
4249
|
+
});
|
|
4250
|
+
}
|
|
4251
|
+
}).transform((ids) => ids);
|
|
3860
4252
|
var RuleSchema = import_zod.z.object({
|
|
3861
|
-
/** Stable: a
|
|
4253
|
+
/** Stable: a guardrail id is never renamed. */
|
|
3862
4254
|
id: import_zod.z.string().min(1),
|
|
3863
|
-
/** The pack this
|
|
4255
|
+
/** The pack this guardrail belongs to. Validated against `PACKS` by the registry. */
|
|
3864
4256
|
category: import_zod.z.string().min(1),
|
|
3865
4257
|
severity: SeveritySchema,
|
|
3866
4258
|
defaultAction: ActionSchema,
|
|
3867
4259
|
title: import_zod.z.string().min(1),
|
|
3868
4260
|
description: import_zod.z.string().min(1),
|
|
4261
|
+
/**
|
|
4262
|
+
* The OWASP risks this guardrail addresses. Optional: a guardrail that cites none omits it. An id is
|
|
4263
|
+
* never a claim that the guardrail covers the whole risk, only that it acts where the risk turns
|
|
4264
|
+
* into a command or a file change.
|
|
4265
|
+
*/
|
|
4266
|
+
owasp: OwaspIdsSchema.optional(),
|
|
3869
4267
|
match: MatchSchema,
|
|
3870
4268
|
fixtures: FixturesSchema
|
|
3871
4269
|
}).strict();
|
|
@@ -3876,7 +4274,7 @@ function parseRuleOrThrow(input) {
|
|
|
3876
4274
|
const result = parseRule(input);
|
|
3877
4275
|
if (!result.success) {
|
|
3878
4276
|
const id = typeof input === "object" && input !== null && "id" in input ? String(input.id) : "<unknown id>";
|
|
3879
|
-
throw new Error(`invalid
|
|
4277
|
+
throw new Error(`invalid guardrail ${id}: ${result.error.message}`);
|
|
3880
4278
|
}
|
|
3881
4279
|
return result.data;
|
|
3882
4280
|
}
|