{
  "schemaVersion": "1.0",
  "about": "AI-specific code and configuration rules for aiwiki_check.py. Each rule names the wiki controls it supports and the test cases that prove the fix. Rules are heuristics that prompt a reviewer; a clean result is not proof that a control is in place.",
  "modelOutputTokens": "\\b(completion|completions|llm_?out\\w*|llm_?resp\\w*|model_?out\\w*|model_?resp\\w*|ai_?out\\w*|ai_?resp\\w*|generated(_?code|_?sql|_?text)?|response\\.(text|content|output_text)|message\\.content|choices\\[0\\]|output_text|tool_?args|tool_?input)\\b",
  "rules": [
    {
      "id": "AIW-001",
      "title": "Model output passed to code or shell execution",
      "kind": "line",
      "severity": "HIGH",
      "files": [".py", ".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx"],
      "pattern": "(\\beval\\s*\\(|\\bexec\\s*\\(|new\\s+Function\\s*\\(|os\\.system\\s*\\(|os\\.popen\\s*\\(|subprocess\\.\\w+\\([^)]*shell\\s*=\\s*True|child_process|\\bexecSync\\s*\\(|\\bspawnSync?\\s*\\()",
      "mustAlsoMatch": "{{modelOutput}}",
      "message": "Text from a model reaches eval, exec or a shell on this line.",
      "remediation": "Never execute model output directly. Parse it into a fixed set of allowed actions, validate the arguments outside the model, and run any generated code in an isolated sandbox with no secrets and no network.",
      "controls": ["AI-CTRL-020", "AI-CTRL-022", "AI-CTRL-027"],
      "risks": ["AI-R07", "AI-R24"],
      "testCases": ["TC-L05-015", "TC-L06-025"]
    },
    {
      "id": "AIW-002",
      "title": "Model output rendered as raw HTML",
      "kind": "line",
      "severity": "MEDIUM",
      "files": [".py", ".js", ".mjs", ".ts", ".tsx", ".jsx", ".vue", ".html", ".jinja", ".j2", ".svelte"],
      "pattern": "(\\.innerHTML\\s*=|\\.outerHTML\\s*=|dangerouslySetInnerHTML|v-html\\s*=|\\{@html\\s|\\|\\s*safe\\b|\\bMarkup\\s*\\(|mark_safe\\s*\\(|unsafe_allow_html\\s*=\\s*True)",
      "mustAlsoMatch": "{{modelOutput}}",
      "message": "Model output is inserted into a page without escaping, which allows script injection and link or image exfiltration.",
      "remediation": "Escape model output by default. If you must render Markdown, sanitise the HTML with an allow-list and block remote images and links to unknown hosts.",
      "controls": ["AI-CTRL-020"],
      "risks": ["AI-R24"],
      "testCases": ["TC-L05-014", "TC-L05-020"]
    },
    {
      "id": "AIW-003",
      "title": "Model output built into a SQL statement",
      "kind": "line",
      "severity": "HIGH",
      "files": [".py", ".js", ".mjs", ".ts", ".java", ".cs", ".go", ".rb", ".php"],
      "pattern": "(\\.execute\\w*\\s*\\(\\s*(f[\"']|[\"'][^\"']*[\"']\\s*(\\+|%)|[\"'][^\"']*[\"']\\.format)|\\.(raw|query)\\s*\\(\\s*`)",
      "mustAlsoMatch": "{{modelOutput}}",
      "message": "A SQL statement is assembled from model output by string formatting.",
      "remediation": "Use parameterised queries, and give the model a fixed set of query templates or a read-only role rather than free SQL.",
      "controls": ["AI-CTRL-020", "AI-CTRL-022"],
      "risks": ["AI-R07", "AI-R24"],
      "testCases": ["TC-L05-015", "TC-L06-014"]
    },
    {
      "id": "AIW-004",
      "title": "Model or data file loaded with unsafe deserialisation",
      "kind": "line",
      "severity": "HIGH",
      "files": [".py", ".ipynb"],
      "pattern": "(\\bpickle\\.loads?\\s*\\(|\\bcloudpickle\\.loads?\\s*\\(|\\bdill\\.loads?\\s*\\(|\\bjoblib\\.load\\s*\\(|\\btorch\\.load\\s*\\(|allow_pickle\\s*=\\s*True)",
      "unlessLine": "weights_only\\s*=\\s*True",
      "message": "This load can run code embedded in the file. A model or dataset from outside your control can execute on load.",
      "remediation": "Prefer safetensors or another format that cannot carry code. For torch.load pass weights_only=True explicitly (check the default for your PyTorch version). Scan third-party model files before loading and load them only from your own registry.",
      "controls": ["AI-CTRL-028", "AI-CTRL-031"],
      "risks": ["AI-R08"],
      "testCases": ["TC-L12-002", "TC-L12-004", "TC-L16-005"]
    },
    {
      "id": "AIW-005",
      "title": "Remote model code trusted at load time",
      "kind": "line",
      "severity": "HIGH",
      "files": [".py", ".ipynb", ".yaml", ".yml", ".json", ".toml"],
      "pattern": "trust_remote_code[\"']?\\s*[=:]\\s*(True|true)",
      "message": "trust_remote_code runs Python shipped with the model repository on your machine.",
      "remediation": "Avoid it. If a model needs it, review the code, pin the exact revision, and load it only in an isolated environment.",
      "controls": ["AI-CTRL-031", "AI-CTRL-027"],
      "risks": ["AI-R08"],
      "testCases": ["TC-L16-003", "TC-L16-004"]
    },
    {
      "id": "AIW-006",
      "title": "Model downloaded without a pinned revision",
      "kind": "line",
      "severity": "LOW",
      "files": [".py", ".ipynb"],
      "pattern": "(\\.from_pretrained\\s*\\(\\s*[\"'][\\w.\\-]+/[\\w.\\-]+[\"']|\\bhf_hub_download\\s*\\(|\\bsnapshot_download\\s*\\()",
      "unlessLine": "\\brevision\\s*=",
      "message": "A hub model is fetched by name only, so a later push to that repository changes what you run.",
      "remediation": "Pass revision= with a commit hash, and record the model, revision and hash in your AI-BOM.",
      "controls": ["AI-CTRL-031"],
      "risks": ["AI-R08", "AI-R52"],
      "testCases": ["TC-L12-018", "TC-L16-001", "TC-L16-003"]
    },
    {
      "id": "AIW-007",
      "title": "MCP server launched from an unpinned package",
      "kind": "mcp",
      "severity": "MEDIUM",
      "message": "The MCP server is started from a package with no fixed version, so a new release runs without review.",
      "remediation": "Pin the exact version (or the image digest), review the server's tool definitions, and record the pin in your tool inventory.",
      "controls": ["AI-CTRL-024", "AI-CTRL-031"],
      "risks": ["AI-R23"],
      "testCases": ["TC-L06-006", "TC-L06-008", "TC-L06-037", "TC-L16-015"]
    },
    {
      "id": "AIW-008",
      "title": "Remote MCP server over plain HTTP",
      "kind": "mcp",
      "severity": "HIGH",
      "message": "A remote MCP server is reached over http://, so tokens and tool results cross the network unencrypted.",
      "remediation": "Use https:// for any server that is not on the loopback interface, and authenticate the connection.",
      "controls": ["AI-CTRL-024", "AI-CTRL-015"],
      "risks": ["AI-R23", "AI-R22"],
      "testCases": ["TC-L06-010"]
    },
    {
      "id": "AIW-009",
      "title": "MCP tools approved automatically",
      "kind": "mcp",
      "severity": "MEDIUM",
      "message": "The client is told to run this server's tools without asking a person.",
      "remediation": "Remove the auto-approval, or limit it to named read-only tools. Keep human approval for any tool that writes, sends or deletes.",
      "controls": ["AI-CTRL-023", "AI-CTRL-024"],
      "risks": ["AI-R07", "AI-R19"],
      "testCases": ["TC-L06-015", "TC-L06-016", "TC-L06-011"]
    },
    {
      "id": "AIW-010",
      "title": "Coding agent run with approvals or sandbox switched off",
      "kind": "line",
      "severity": "MEDIUM",
      "files": [".sh", ".bash", ".zsh", ".ps1", ".bat", ".cmd", ".yml", ".yaml", ".json", ".toml", "Dockerfile", "Makefile", ".py", ".js", ".ts"],
      "pattern": "(--dangerously-skip-permissions|--dangerously-bypass-approvals-and-sandbox|--yolo\\b|--approval-mode[= ]+yolo|--full-auto\\b|\"autoApprove\"\\s*:\\s*true|\"chat\\.tools\\.autoApprove\"\\s*:\\s*true)",
      "message": "A coding agent is started with its approval prompts or sandbox disabled.",
      "remediation": "Run unattended agents only inside a disposable container with no production credentials and an egress allow-list, and keep the flag out of developer machines and shared scripts.",
      "controls": ["AI-CTRL-023", "AI-CTRL-027", "AI-CTRL-004"],
      "risks": ["AI-R04", "AI-R07", "AI-R22"],
      "testCases": ["TC-L06-036", "TC-L06-025"]
    }
  ]
}
