codegate-ai 0.16.2 → 1.0.1

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.
Files changed (75) hide show
  1. package/README.md +13 -12
  2. package/dist/cli.d.ts +3 -1
  3. package/dist/cli.js +153 -44
  4. package/dist/commands/scan-command.d.ts +2 -1
  5. package/dist/commands/scan-command.js +6 -1
  6. package/dist/commands/trust.d.ts +28 -0
  7. package/dist/commands/trust.js +69 -0
  8. package/dist/config/inline-ignore.d.ts +10 -2
  9. package/dist/config/inline-ignore.js +5 -1
  10. package/dist/config/suppression-policy.d.ts +1 -1
  11. package/dist/config/suppression-policy.js +4 -1
  12. package/dist/config/trust.d.ts +2 -0
  13. package/dist/config/trust.js +19 -0
  14. package/dist/config.d.ts +14 -0
  15. package/dist/config.js +45 -1
  16. package/dist/content/content-bundle.d.ts +27 -0
  17. package/dist/content/content-bundle.js +73 -0
  18. package/dist/content/content-store.d.ts +27 -0
  19. package/dist/content/content-store.js +0 -0
  20. package/dist/content/content-updater.d.ts +32 -0
  21. package/dist/content/content-updater.js +111 -0
  22. package/dist/content/known-bad.d.ts +26 -0
  23. package/dist/content/known-bad.js +100 -0
  24. package/dist/content/publisher-key.d.ts +10 -0
  25. package/dist/content/publisher-key.js +10 -0
  26. package/dist/layer1-discovery/knowledge-base.js +33 -1
  27. package/dist/layer2-static/data/popular-mcp-packages.d.ts +16 -0
  28. package/dist/layer2-static/data/popular-mcp-packages.js +83 -0
  29. package/dist/layer2-static/detectors/known-bad.d.ts +22 -0
  30. package/dist/layer2-static/detectors/known-bad.js +116 -0
  31. package/dist/layer2-static/detectors/mcp-package-hygiene.d.ts +28 -0
  32. package/dist/layer2-static/detectors/mcp-package-hygiene.js +191 -0
  33. package/dist/layer2-static/detectors/rule-file.js +128 -75
  34. package/dist/layer2-static/detectors/skill-frontmatter.d.ts +6 -0
  35. package/dist/layer2-static/detectors/skill-frontmatter.js +130 -0
  36. package/dist/layer2-static/engine.d.ts +2 -0
  37. package/dist/layer2-static/engine.js +0 -0
  38. package/dist/layer2-static/rule-pack-loader.js +24 -1
  39. package/dist/layer2-static/state/scan-state.d.ts +7 -2
  40. package/dist/layer2-static/state/scan-state.js +74 -30
  41. package/dist/layer2-static/text/confusables.d.ts +7 -0
  42. package/dist/layer2-static/text/confusables.js +70 -0
  43. package/dist/layer2-static/text/edit-distance.d.ts +6 -0
  44. package/dist/layer2-static/text/edit-distance.js +33 -0
  45. package/dist/layer2-static/text/encoded-payloads.d.ts +16 -0
  46. package/dist/layer2-static/text/encoded-payloads.js +116 -0
  47. package/dist/layer2-static/text/normalize.d.ts +11 -0
  48. package/dist/layer2-static/text/normalize.js +19 -0
  49. package/dist/layer2-static/text/override-phrases.d.ts +14 -0
  50. package/dist/layer2-static/text/override-phrases.js +49 -0
  51. package/dist/layer2-static/text/threat-patterns.d.ts +33 -0
  52. package/dist/layer2-static/text/threat-patterns.js +45 -0
  53. package/dist/layer2-static/text/unicode.d.ts +33 -0
  54. package/dist/layer2-static/text/unicode.js +83 -0
  55. package/dist/layer3-dynamic/deep-resource-executor.d.ts +21 -0
  56. package/dist/layer3-dynamic/deep-resource-executor.js +73 -0
  57. package/dist/layer3-dynamic/meta-agent.js +2 -1
  58. package/dist/layer3-dynamic/registry-client.d.ts +26 -0
  59. package/dist/layer3-dynamic/registry-client.js +138 -0
  60. package/dist/layer3-dynamic/registry-findings.d.ts +7 -0
  61. package/dist/layer3-dynamic/registry-findings.js +64 -0
  62. package/dist/layer3-dynamic/tool-description-scanner.js +22 -13
  63. package/dist/layer3-dynamic/toxic-flow.d.ts +4 -0
  64. package/dist/layer3-dynamic/toxic-flow.js +41 -8
  65. package/dist/pipeline.d.ts +5 -2
  66. package/dist/pipeline.js +53 -16
  67. package/dist/report-summary.d.ts +1 -1
  68. package/dist/report-summary.js +9 -1
  69. package/dist/scan.d.ts +13 -0
  70. package/dist/scan.js +291 -17
  71. package/dist/types/finding.d.ts +14 -0
  72. package/dist/types/finding.js +14 -0
  73. package/dist/types/report.d.ts +2 -0
  74. package/dist/wrapper.js +2 -19
  75. package/package.json +1 -1
package/README.md CHANGED
@@ -104,18 +104,19 @@ See the [Configuration](#configuration) section for full settings and examples.
104
104
 
105
105
  ## Core Commands
106
106
 
107
- | Command | Purpose |
108
- | ------------------------ | ---------------------------------------------------------------------- |
109
- | `codegate scan [target]` | Scan a directory, file, or URL target for AI tool config risks. |
110
- | `codegate scan-content` | Scan inline JSON, YAML, TOML, Markdown, or text content. |
111
- | `codegate run <tool>` | Scan current directory, then launch selected AI tool if policy allows. |
112
- | `codegate skills [...]` | Wrap `npx skills` and preflight-scan `skills add` targets. |
113
- | `codegate clawhub [...]` | Wrap `npx clawhub` and preflight-scan `clawhub install` targets. |
114
- | `codegate undo [dir]` | Restore the most recent remediation backup session. Defaults to `.`. |
115
- | `codegate init` | Create `~/.codegate/config.json` with defaults. |
116
- | `codegate update-kb` | Show knowledge-base update guidance. |
117
- | `codegate update-rules` | Show rules update guidance. |
118
- | `codegate --help` | Show CLI usage. |
107
+ | Command | Purpose |
108
+ | ------------------------ | ----------------------------------------------------------------------------------- |
109
+ | `codegate scan [target]` | Scan a directory, file, or URL target for AI tool config risks. |
110
+ | `codegate scan-content` | Scan inline JSON, YAML, TOML, Markdown, or text content. |
111
+ | `codegate run <tool>` | Scan current directory, then launch selected AI tool if policy allows. |
112
+ | `codegate skills [...]` | Wrap `npx skills` and preflight-scan `skills add` targets. |
113
+ | `codegate clawhub [...]` | Wrap `npx clawhub` and preflight-scan `clawhub install` targets. |
114
+ | `codegate undo [dir]` | Restore the most recent remediation backup session. Defaults to `.`. |
115
+ | `codegate init` | Create `~/.codegate/config.json` with defaults. |
116
+ | `codegate trust [dir]` | Trust a directory so its `.codegate.json` policy keys are honored. |
117
+ | `codegate update-kb` | Fetch, verify, and install signed knowledge-base content (`--check`, `--rollback`). |
118
+ | `codegate update-rules` | Fetch, verify, and install signed detection content (`--check`, `--rollback`). |
119
+ | `codegate --help` | Show CLI usage. |
119
120
 
120
121
  ## `scan` Command Flags
121
122
 
package/dist/cli.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
3
  import { type CodeGateConfig, type ResolveConfigOptions } from "./config.js";
4
+ import { type DeepResourceExecutionContext } from "./layer3-dynamic/deep-resource-executor.js";
4
5
  import type { ResourceFetchResult } from "./layer3-dynamic/resource-fetcher.js";
5
6
  import type { LocalTextAnalysisTarget } from "./layer3-dynamic/local-text-analysis.js";
6
7
  import { type DeepScanResource } from "./pipeline.js";
@@ -55,8 +56,9 @@ export interface CliDeps {
55
56
  runMetaAgentCommand?: (context: MetaAgentCommandConsentContext) => Promise<MetaAgentCommandRunResult> | MetaAgentCommandRunResult;
56
57
  requestRemediationConsent?: (context: RemediationConsentContext) => Promise<boolean> | boolean;
57
58
  requestRunWarningConsent?: (context: RunWarningConsentContext) => Promise<boolean> | boolean;
59
+ requestTrustConsent?: (directory: string) => Promise<boolean> | boolean;
58
60
  requestSkillSelection?: (options: string[]) => Promise<string | null> | string | null;
59
- executeDeepResource?: (resource: DeepScanResource) => Promise<ResourceFetchResult>;
61
+ executeDeepResource?: (resource: DeepScanResource, context?: DeepResourceExecutionContext) => Promise<ResourceFetchResult>;
60
62
  launchSkills?: (args: string[], cwd: string) => SkillsWrapperLaunchResult;
61
63
  launchClawhub?: (args: string[], cwd: string) => ClawhubWrapperLaunchResult;
62
64
  runSkillsWrapper?: (input: {
package/dist/cli.js CHANGED
@@ -8,6 +8,7 @@ import { pathToFileURL } from "node:url";
8
8
  import { Command, Option } from "commander";
9
9
  import { DEFAULT_CONFIG, OUTPUT_FORMATS, PERSONAS, RUNTIME_MODES, SCAN_COLLECTION_MODES, SCAN_COLLECTION_KINDS, resolveEffectiveConfig, } from "./config.js";
10
10
  import { APP_NAME } from "./index.js";
11
+ import { createDeepResourceExecutor, } from "./layer3-dynamic/deep-resource-executor.js";
11
12
  import { runSandboxCommand } from "./layer3-dynamic/sandbox.js";
12
13
  import { runClaudeViaSdk } from "./layer3-dynamic/claude-sdk-provider.js";
13
14
  import { loadKnowledgeBase } from "./layer1-discovery/knowledge-base.js";
@@ -18,6 +19,8 @@ import { renderTuiApp } from "./tui/app.js";
18
19
  import { executeWrapperRun } from "./wrapper.js";
19
20
  import { runRemediation as runRemediationWorkflow, } from "./layer4-remediation/remediation-runner.js";
20
21
  import { undoLatestSession } from "./commands/undo.js";
22
+ import { addTrustedDirectory, listTrustedDirectories, removeTrustedDirectory, } from "./commands/trust.js";
23
+ import { checkContentUpdate, rollbackContent, updateContent } from "./content/content-updater.js";
21
24
  import { runInventory } from "./commands/inventory-command.js";
22
25
  import { executeScanCommand } from "./commands/scan-command.js";
23
26
  import { executeScanContentCommand, SCAN_CONTENT_TYPES, } from "./commands/scan-content-command.js";
@@ -206,23 +209,10 @@ const defaultCliDeps = {
206
209
  collectKinds: config?.scan_collection_kinds,
207
210
  }),
208
211
  discoverLocalTextTargets: (_scanTarget, _config, discoveryContext) => discoveryContext ? discoverLocalTextAnalysisTargetsFromContext(discoveryContext) : [],
209
- // Deep resource execution never makes outbound network calls.
210
- // Connecting to URLs found in scanned config files is a security risk:
211
- // the endpoint could be malicious (crafted responses, SSRF, IP logging).
212
- // Instead, we record the URL as metadata for the agent to analyze.
213
- executeDeepResource: async (resource) => {
214
- return {
215
- status: "ok",
216
- attempts: 0,
217
- elapsedMs: 0,
218
- metadata: {
219
- resource_id: resource.id,
220
- resource_kind: resource.request.kind,
221
- resource_url: resource.request.locator,
222
- note: "URL recorded for analysis without making outbound connections.",
223
- },
224
- };
225
- },
212
+ // URL resources are never fetched (SSRF/IP-logging risk); npm/pypi registry
213
+ // metadata is fetched from pinned hosts only when runtime_mode is "online".
214
+ // See createDeepResourceExecutor for the full policy.
215
+ executeDeepResource: createDeepResourceExecutor(),
226
216
  launchSkills: (args, cwd) => launchSkillsPassthrough(args, cwd),
227
217
  launchClawhub: (args, cwd) => launchClawhubPassthrough(args, cwd),
228
218
  };
@@ -604,6 +594,100 @@ function addClawhubCommand(program, version, deps) {
604
594
  }
605
595
  });
606
596
  }
597
+ async function promptTrustConsent(directory) {
598
+ const rl = createInterface({
599
+ input: process.stdin,
600
+ output: process.stdout,
601
+ });
602
+ const prompt = [
603
+ `Trusting ${directory} lets its .codegate.json set scanner policy`,
604
+ "(allowlists, rule skips, suppressions) and relaxes launch gating for it.",
605
+ "Only trust directories whose contents you control.",
606
+ "Proceed? [y/N]: ",
607
+ ].join("\n");
608
+ try {
609
+ const answer = await rl.question(prompt);
610
+ return /^y(es)?$/iu.test(answer.trim());
611
+ }
612
+ finally {
613
+ rl.close();
614
+ }
615
+ }
616
+ function addTrustCommand(program, deps) {
617
+ program
618
+ .command("trust [dir]")
619
+ .description("Manage trusted directories where project config policy is honored")
620
+ .option("--list", "list trusted directories")
621
+ .option("--remove <dir>", "remove a directory from the trusted list")
622
+ .option("--config <path>", "use a specific global config file")
623
+ .option("--yes", "skip the confirmation prompt")
624
+ .addHelpText("after", renderExampleHelp([
625
+ "codegate trust",
626
+ "codegate trust ./project --yes",
627
+ "codegate trust --list",
628
+ "codegate trust --remove ./project",
629
+ ]))
630
+ .action(async (dir, options) => {
631
+ try {
632
+ if (options.list) {
633
+ const listed = listTrustedDirectories({ configPath: options.config });
634
+ if (listed.trustedDirectories.length === 0) {
635
+ deps.stdout(`No trusted directories configured (${listed.configPath}).`);
636
+ }
637
+ else {
638
+ deps.stdout(`Trusted directories (${listed.configPath}):`);
639
+ for (const entry of listed.trustedDirectories) {
640
+ deps.stdout(` ${entry}`);
641
+ }
642
+ }
643
+ deps.setExitCode(0);
644
+ return;
645
+ }
646
+ if (options.remove) {
647
+ const removed = removeTrustedDirectory({
648
+ dir: options.remove,
649
+ cwd: deps.cwd(),
650
+ configPath: options.config,
651
+ });
652
+ deps.stdout(removed.changed
653
+ ? `Removed ${removed.directory} from trusted directories.`
654
+ : `${removed.directory} was not in the trusted list.`);
655
+ deps.setExitCode(0);
656
+ return;
657
+ }
658
+ const targetDir = dir ?? ".";
659
+ const resolvedPreview = resolve(deps.cwd(), targetDir);
660
+ if (!options.yes) {
661
+ const requestConsent = deps.requestTrustConsent ?? (deps.isTTY() ? promptTrustConsent : undefined);
662
+ if (!requestConsent) {
663
+ deps.stderr("Confirmation required. Re-run with --yes to trust non-interactively.");
664
+ deps.setExitCode(3);
665
+ return;
666
+ }
667
+ const approved = await requestConsent(resolvedPreview);
668
+ if (!approved) {
669
+ deps.stdout("Trust not granted.");
670
+ deps.setExitCode(1);
671
+ return;
672
+ }
673
+ }
674
+ const added = addTrustedDirectory({
675
+ dir: targetDir,
676
+ cwd: deps.cwd(),
677
+ configPath: options.config,
678
+ });
679
+ deps.stdout(added.changed
680
+ ? `Trusted ${added.directory} (${added.configPath}).`
681
+ : `${added.directory} is already trusted.`);
682
+ deps.setExitCode(0);
683
+ }
684
+ catch (error) {
685
+ const message = error instanceof Error ? error.message : String(error);
686
+ deps.stderr(`Trust failed: ${message}`);
687
+ deps.setExitCode(3);
688
+ }
689
+ });
690
+ }
607
691
  function addUndoCommand(program, deps) {
608
692
  program
609
693
  .command("undo [dir]")
@@ -723,33 +807,57 @@ function renderInventoryText(summary, stdout) {
723
807
  }
724
808
  }
725
809
  function addUpdateCommands(program, deps) {
726
- const guidance = [
727
- "Updates are bundled with CodeGate releases in v1/v2.",
728
- "Run: npm update -g codegate-ai",
729
- "Or run latest directly: npx codegate-ai@latest scan .",
730
- ];
731
- program
732
- .command("update-kb")
733
- .description("Check for newer knowledge-base content")
734
- .addHelpText("after", renderExampleHelp(["codegate update-kb"]))
735
- .action(() => {
736
- deps.stdout("update-kb:");
737
- for (const line of guidance) {
738
- deps.stdout(line);
739
- }
740
- deps.setExitCode(0);
741
- });
742
- program
743
- .command("update-rules")
744
- .description("Check for newer rules content")
745
- .addHelpText("after", renderExampleHelp(["codegate update-rules"]))
746
- .action(() => {
747
- deps.stdout("update-rules:");
748
- for (const line of guidance) {
749
- deps.stdout(line);
750
- }
751
- deps.setExitCode(0);
752
- });
810
+ const registerUpdateCommand = (name, description) => {
811
+ program
812
+ .command(name)
813
+ .description(description)
814
+ .option("--check", "check for a newer signed content bundle without installing")
815
+ .option("--rollback", "switch back to the previously installed content version")
816
+ .option("--url <baseUrl>", "override the content download base URL (the signature is still required to verify)")
817
+ .addHelpText("after", renderExampleHelp([
818
+ `codegate ${name}`,
819
+ `codegate ${name} --check`,
820
+ `codegate ${name} --rollback`,
821
+ ]))
822
+ .action(async (options) => {
823
+ try {
824
+ if (options.rollback) {
825
+ const rolledBack = rollbackContent();
826
+ deps.stdout(`Rolled back to content version ${rolledBack.version}.`);
827
+ deps.setExitCode(0);
828
+ return;
829
+ }
830
+ if (options.check) {
831
+ const checked = await checkContentUpdate({}, { baseUrl: options.url });
832
+ deps.stdout(`Installed content: ${checked.currentVersion ?? "bundled only"}`);
833
+ deps.stdout(`Latest published: ${checked.remoteVersion}`);
834
+ deps.stdout(checked.updateAvailable
835
+ ? `Update available. Run: codegate ${name}`
836
+ : "Content is up to date.");
837
+ deps.setExitCode(0);
838
+ return;
839
+ }
840
+ const updated = await updateContent({}, { baseUrl: options.url });
841
+ if (updated.changed) {
842
+ deps.stdout(`Updated content: ${updated.previousVersion ?? "bundled only"} -> ${updated.version}`);
843
+ if (updated.pruned.length > 0) {
844
+ deps.stdout(`Pruned old versions: ${updated.pruned.join(", ")}`);
845
+ }
846
+ }
847
+ else {
848
+ deps.stdout(`Content already up to date (${updated.version}).`);
849
+ }
850
+ deps.setExitCode(0);
851
+ }
852
+ catch (error) {
853
+ const message = error instanceof Error ? error.message : String(error);
854
+ deps.stderr(`${name} failed: ${message}`);
855
+ deps.setExitCode(3);
856
+ }
857
+ });
858
+ };
859
+ registerUpdateCommand("update-kb", "Fetch and verify the signed content bundle (knowledge base, rules, phrase lists)");
860
+ registerUpdateCommand("update-rules", "Alias of update-kb: content ships as one signed bundle");
753
861
  }
754
862
  function resolveKnowledgeBaseVersion() {
755
863
  try {
@@ -781,6 +889,7 @@ export function createCli(version = packageJson.version ?? "0.0.0-dev", deps = d
781
889
  addSkillsCommand(program, version, deps);
782
890
  addClawhubCommand(program, version, deps);
783
891
  addRunCommand(program, version, deps);
892
+ addTrustCommand(program, deps);
784
893
  addUndoCommand(program, deps);
785
894
  addInitCommand(program, deps);
786
895
  addInventoryCommand(program, deps);
@@ -1,6 +1,7 @@
1
1
  import { type AuditPersona, type CodeGateConfig, type OutputFormat, type RuntimeMode, type ScanCollectionMode } from "../config.js";
2
2
  import { type MetaAgentCommand, type MetaAgentTool } from "../layer3-dynamic/command-builder.js";
3
3
  import type { LocalTextAnalysisTarget } from "../layer3-dynamic/local-text-analysis.js";
4
+ import type { DeepResourceExecutionContext } from "../layer3-dynamic/deep-resource-executor.js";
4
5
  import type { ResourceFetchResult } from "../layer3-dynamic/resource-fetcher.js";
5
6
  import { type DeepScanResource } from "../pipeline.js";
6
7
  import type { ScanDiscoveryCandidate, ScanDiscoveryContext } from "../scan.js";
@@ -81,7 +82,7 @@ export interface ExecuteScanCommandDeps {
81
82
  requestMetaAgentCommandConsent?: (context: MetaAgentCommandConsentContext) => Promise<boolean> | boolean;
82
83
  runMetaAgentCommand?: (context: MetaAgentCommandConsentContext) => Promise<MetaAgentCommandRunResult> | MetaAgentCommandRunResult;
83
84
  requestRemediationConsent?: (context: RemediationConsentContext) => Promise<boolean> | boolean;
84
- executeDeepResource?: (resource: DeepScanResource) => Promise<ResourceFetchResult>;
85
+ executeDeepResource?: (resource: DeepScanResource, context?: DeepResourceExecutionContext) => Promise<ResourceFetchResult>;
85
86
  runRemediation?: (input: RemediationRunnerInput) => Promise<RemediationRunnerResult> | RemediationRunnerResult;
86
87
  stdout: (message: string) => void;
87
88
  stderr: (message: string) => void;
@@ -130,7 +130,9 @@ export async function runScanAnalysis(input, deps) {
130
130
  }
131
131
  return false;
132
132
  }, async (resource) => {
133
- const fetched = await deps.executeDeepResource(resource);
133
+ const fetched = await deps.executeDeepResource(resource, {
134
+ runtimeMode: input.config.runtime_mode,
135
+ });
134
136
  if (fetched.status !== "ok" || !selectedAgent) {
135
137
  return fetched;
136
138
  }
@@ -202,6 +204,9 @@ export async function runScanAnalysis(input, deps) {
202
204
  });
203
205
  const layer3Findings = layer3OutcomesToFindings(outcomes, {
204
206
  unicodeAnalysis: input.config.unicode_analysis,
207
+ registryHeuristics: {
208
+ recentPublishDays: input.config.registry_heuristics?.recent_publish_days,
209
+ },
205
210
  });
206
211
  report = mergeLayer3Findings(report, layer3Findings);
207
212
  if (selectedAgent) {
@@ -0,0 +1,28 @@
1
+ export interface TrustStoreDeps {
2
+ homeDir: () => string;
3
+ pathExists: (path: string) => boolean;
4
+ readFile: (path: string) => string;
5
+ writeFile: (path: string, content: string) => void;
6
+ }
7
+ export interface TrustActionResult {
8
+ configPath: string;
9
+ directory: string;
10
+ changed: boolean;
11
+ trustedDirectories: string[];
12
+ }
13
+ export declare function listTrustedDirectories(input?: {
14
+ configPath?: string;
15
+ }, deps?: TrustStoreDeps): {
16
+ configPath: string;
17
+ trustedDirectories: string[];
18
+ };
19
+ export declare function addTrustedDirectory(input: {
20
+ dir: string;
21
+ cwd: string;
22
+ configPath?: string;
23
+ }, deps?: TrustStoreDeps): TrustActionResult;
24
+ export declare function removeTrustedDirectory(input: {
25
+ dir: string;
26
+ cwd: string;
27
+ configPath?: string;
28
+ }, deps?: TrustStoreDeps): TrustActionResult;
@@ -0,0 +1,69 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { parse as parseJsonc } from "jsonc-parser";
5
+ import { expandHomePath } from "../config/trust.js";
6
+ const defaultTrustStoreDeps = {
7
+ homeDir: () => homedir(),
8
+ pathExists: (path) => existsSync(path),
9
+ readFile: (path) => readFileSync(path, "utf8"),
10
+ writeFile: (path, content) => {
11
+ mkdirSync(dirname(path), { recursive: true });
12
+ writeFileSync(path, content, "utf8");
13
+ },
14
+ };
15
+ function resolveConfigPath(configPath, deps) {
16
+ return configPath ?? join(deps.homeDir(), ".codegate", "config.json");
17
+ }
18
+ function readConfigObject(path, deps) {
19
+ if (!deps.pathExists(path)) {
20
+ return {};
21
+ }
22
+ const parsed = parseJsonc(deps.readFile(path));
23
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
24
+ throw new Error(`Invalid config file: ${path}`);
25
+ }
26
+ return parsed;
27
+ }
28
+ function readTrustedList(config) {
29
+ const raw = config.trusted_directories;
30
+ if (!Array.isArray(raw)) {
31
+ return [];
32
+ }
33
+ return raw.filter((entry) => typeof entry === "string" && entry.length > 0);
34
+ }
35
+ function writeConfigObject(path, config, deps) {
36
+ deps.writeFile(path, `${JSON.stringify(config, null, 2)}\n`);
37
+ }
38
+ function sameDirectory(left, right) {
39
+ return resolve(expandHomePath(left)) === resolve(expandHomePath(right));
40
+ }
41
+ export function listTrustedDirectories(input = {}, deps = defaultTrustStoreDeps) {
42
+ const configPath = resolveConfigPath(input.configPath, deps);
43
+ const config = readConfigObject(configPath, deps);
44
+ return { configPath, trustedDirectories: readTrustedList(config) };
45
+ }
46
+ export function addTrustedDirectory(input, deps = defaultTrustStoreDeps) {
47
+ const directory = resolve(input.cwd, expandHomePath(input.dir));
48
+ const configPath = resolveConfigPath(input.configPath, deps);
49
+ const config = readConfigObject(configPath, deps);
50
+ const trusted = readTrustedList(config);
51
+ if (trusted.some((entry) => sameDirectory(entry, directory))) {
52
+ return { configPath, directory, changed: false, trustedDirectories: trusted };
53
+ }
54
+ const next = [...trusted, directory];
55
+ writeConfigObject(configPath, { ...config, trusted_directories: next }, deps);
56
+ return { configPath, directory, changed: true, trustedDirectories: next };
57
+ }
58
+ export function removeTrustedDirectory(input, deps = defaultTrustStoreDeps) {
59
+ const directory = resolve(input.cwd, expandHomePath(input.dir));
60
+ const configPath = resolveConfigPath(input.configPath, deps);
61
+ const config = readConfigObject(configPath, deps);
62
+ const trusted = readTrustedList(config);
63
+ const next = trusted.filter((entry) => !sameDirectory(entry, directory));
64
+ if (next.length === trusted.length) {
65
+ return { configPath, directory, changed: false, trustedDirectories: trusted };
66
+ }
67
+ writeConfigObject(configPath, { ...config, trusted_directories: next }, deps);
68
+ return { configPath, directory, changed: true, trustedDirectories: next };
69
+ }
@@ -1,4 +1,4 @@
1
- import type { Finding } from "../types/finding.js";
1
+ import { type Finding } from "../types/finding.js";
2
2
  export interface InlineIgnoreDirectiveSet {
3
3
  rules: Set<string>;
4
4
  ruleLines: Map<string, Set<number>>;
@@ -8,4 +8,12 @@ export declare function collectInlineIgnoreDirectives(files: Array<{
8
8
  filePath: string;
9
9
  textContent: string;
10
10
  }>): InlineIgnoreMap;
11
- export declare function applyInlineIgnoreDirectives<T extends Finding>(findings: T[], directives: InlineIgnoreMap): T[];
11
+ export interface ApplyInlineIgnoreOptions {
12
+ /**
13
+ * Whether the scanned target is trusted. Inline directives in untrusted
14
+ * content are recorded but must not weaken gating, so they are tagged
15
+ * "inline-untrusted" and still count toward the exit code.
16
+ */
17
+ trustedTarget: boolean;
18
+ }
19
+ export declare function applyInlineIgnoreDirectives<T extends Finding>(findings: T[], directives: InlineIgnoreMap, options?: ApplyInlineIgnoreOptions): T[];
@@ -1,3 +1,4 @@
1
+ import { SUPPRESSION_SOURCE } from "../types/finding.js";
1
2
  function normalizeRuleId(value) {
2
3
  const trimmed = value.trim();
3
4
  return trimmed.length > 0 ? trimmed : null;
@@ -43,7 +44,7 @@ export function collectInlineIgnoreDirectives(files) {
43
44
  }
44
45
  return directives;
45
46
  }
46
- export function applyInlineIgnoreDirectives(findings, directives) {
47
+ export function applyInlineIgnoreDirectives(findings, directives, options = { trustedTarget: true }) {
47
48
  return findings.map((finding) => {
48
49
  const set = directives.get(finding.file_path);
49
50
  if (!set || !set.rules.has(finding.rule_id)) {
@@ -52,6 +53,9 @@ export function applyInlineIgnoreDirectives(findings, directives) {
52
53
  return {
53
54
  ...finding,
54
55
  suppressed: true,
56
+ suppression_source: options.trustedTarget
57
+ ? SUPPRESSION_SOURCE.Inline
58
+ : SUPPRESSION_SOURCE.InlineUntrusted,
55
59
  };
56
60
  });
57
61
  }
@@ -1,4 +1,4 @@
1
- import type { Finding } from "../types/finding.js";
1
+ import { type Finding } from "../types/finding.js";
2
2
  export interface SuppressionRule {
3
3
  rule_id?: string;
4
4
  file_path?: string;
@@ -1,3 +1,4 @@
1
+ import { SUPPRESSION_SOURCE } from "../types/finding.js";
1
2
  function normalizeString(value) {
2
3
  if (typeof value !== "string") {
3
4
  return undefined;
@@ -138,9 +139,11 @@ export function applySuppressionPolicy(findings, policy) {
138
139
  const rulePolicy = rulePolicies[finding.rule_id];
139
140
  const ruleDisabled = rulePolicy?.disable === true;
140
141
  const ruleIgnoreMatch = rulePolicy?.ignore?.some((location) => matchesRulePolicyIgnore(finding, location)) ?? false;
142
+ const policyMatch = legacyMatch || ruleMatch || ruleDisabled || ruleIgnoreMatch;
141
143
  return {
142
144
  ...finding,
143
- suppressed: finding.suppressed || legacyMatch || ruleMatch || ruleDisabled || ruleIgnoreMatch,
145
+ suppressed: finding.suppressed || policyMatch,
146
+ suppression_source: finding.suppression_source ?? (policyMatch ? SUPPRESSION_SOURCE.Config : undefined),
144
147
  };
145
148
  });
146
149
  }
@@ -0,0 +1,2 @@
1
+ export declare function expandHomePath(path: string): string;
2
+ export declare function isTrustedDirectory(target: string, trustedDirectories: string[]): boolean;
@@ -0,0 +1,19 @@
1
+ import { homedir } from "node:os";
2
+ import { isAbsolute, relative, resolve, sep } from "node:path";
3
+ export function expandHomePath(path) {
4
+ if (path === "~") {
5
+ return homedir();
6
+ }
7
+ if (path.startsWith(`~${sep}`) || path.startsWith("~/")) {
8
+ return resolve(homedir(), path.slice(2));
9
+ }
10
+ return path;
11
+ }
12
+ export function isTrustedDirectory(target, trustedDirectories) {
13
+ const resolvedTarget = resolve(target);
14
+ return trustedDirectories.some((trustedPath) => {
15
+ const resolvedTrusted = resolve(expandHomePath(trustedPath));
16
+ const rel = relative(resolvedTrusted, resolvedTarget);
17
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
18
+ });
19
+ }
package/dist/config.d.ts CHANGED
@@ -26,6 +26,9 @@ export interface ToolDiscoveryConfig {
26
26
  export interface WorkflowAuditConfig {
27
27
  enabled: boolean;
28
28
  }
29
+ export interface RegistryHeuristicsConfig {
30
+ recent_publish_days: number;
31
+ }
29
32
  export interface CodeGateConfig {
30
33
  severity_threshold: SeverityThreshold;
31
34
  auto_proceed_below_threshold: boolean;
@@ -42,6 +45,8 @@ export interface CodeGateConfig {
42
45
  known_safe_hooks: string[];
43
46
  unicode_analysis: boolean;
44
47
  check_ide_settings: boolean;
48
+ /** Emit review findings for MCP servers seen for the first time in a project. */
49
+ first_scan_review?: boolean;
45
50
  owasp_mapping: boolean;
46
51
  trusted_api_domains: string[];
47
52
  rule_pack_paths?: string[];
@@ -54,6 +59,7 @@ export interface CodeGateConfig {
54
59
  persona?: AuditPersona;
55
60
  runtime_mode?: RuntimeMode;
56
61
  workflow_audits?: WorkflowAuditConfig;
62
+ registry_heuristics?: RegistryHeuristicsConfig;
57
63
  suppress_findings: string[];
58
64
  suppression_rules?: SuppressionRule[];
59
65
  /**
@@ -72,6 +78,10 @@ export interface CodeGateConfig {
72
78
  * `CODEGATE_LAYER3_REMOTE_FETCH_MAX_BYTES`.
73
79
  */
74
80
  layer3_remote_fetch_max_bytes: number;
81
+ /** True when the scan target sits inside a globally trusted directory. */
82
+ project_config_trusted?: boolean;
83
+ /** Policy keys present in the target's .codegate.json that were ignored because the target is untrusted. */
84
+ ignored_project_settings?: string[];
75
85
  }
76
86
  export interface CliConfigOverrides {
77
87
  format?: OutputFormat;
@@ -88,6 +98,10 @@ export declare const DEFAULT_CONFIG: CodeGateConfig;
88
98
  export declare const LAYER3_REMOTE_FETCH_TIMEOUT_ENV = "CODEGATE_LAYER3_REMOTE_FETCH_TIMEOUT_MS";
89
99
  /** Env var name that overrides `layer3_remote_fetch_max_bytes`. */
90
100
  export declare const LAYER3_REMOTE_FETCH_MAX_BYTES_ENV = "CODEGATE_LAYER3_REMOTE_FETCH_MAX_BYTES";
101
+ export declare const PROJECT_COSMETIC_KEYS: readonly ["output_format", "tui", "owasp_mapping"];
102
+ export type ProjectCosmeticKey = (typeof PROJECT_COSMETIC_KEYS)[number];
103
+ export declare const PROJECT_FORBIDDEN_KEYS: readonly ["trusted_directories"];
104
+ export type ProjectForbiddenKey = (typeof PROJECT_FORBIDDEN_KEYS)[number];
91
105
  export declare function resolveEffectiveConfig(options: ResolveConfigOptions): CodeGateConfig;
92
106
  export declare function computeExitCode(findings: Finding[], threshold: SeverityThreshold): number;
93
107
  export declare function applyConfigPolicy(report: CodeGateReport, config: CodeGateConfig): CodeGateReport;