codegate-ai 0.16.2 → 1.0.0

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 (74) hide show
  1. package/dist/cli.d.ts +3 -1
  2. package/dist/cli.js +153 -44
  3. package/dist/commands/scan-command.d.ts +2 -1
  4. package/dist/commands/scan-command.js +6 -1
  5. package/dist/commands/trust.d.ts +28 -0
  6. package/dist/commands/trust.js +69 -0
  7. package/dist/config/inline-ignore.d.ts +10 -2
  8. package/dist/config/inline-ignore.js +5 -1
  9. package/dist/config/suppression-policy.d.ts +1 -1
  10. package/dist/config/suppression-policy.js +4 -1
  11. package/dist/config/trust.d.ts +2 -0
  12. package/dist/config/trust.js +19 -0
  13. package/dist/config.d.ts +14 -0
  14. package/dist/config.js +45 -1
  15. package/dist/content/content-bundle.d.ts +27 -0
  16. package/dist/content/content-bundle.js +73 -0
  17. package/dist/content/content-store.d.ts +27 -0
  18. package/dist/content/content-store.js +0 -0
  19. package/dist/content/content-updater.d.ts +32 -0
  20. package/dist/content/content-updater.js +111 -0
  21. package/dist/content/known-bad.d.ts +26 -0
  22. package/dist/content/known-bad.js +100 -0
  23. package/dist/content/publisher-key.d.ts +10 -0
  24. package/dist/content/publisher-key.js +10 -0
  25. package/dist/layer1-discovery/knowledge-base.js +33 -1
  26. package/dist/layer2-static/data/popular-mcp-packages.d.ts +16 -0
  27. package/dist/layer2-static/data/popular-mcp-packages.js +83 -0
  28. package/dist/layer2-static/detectors/known-bad.d.ts +22 -0
  29. package/dist/layer2-static/detectors/known-bad.js +116 -0
  30. package/dist/layer2-static/detectors/mcp-package-hygiene.d.ts +28 -0
  31. package/dist/layer2-static/detectors/mcp-package-hygiene.js +191 -0
  32. package/dist/layer2-static/detectors/rule-file.js +128 -75
  33. package/dist/layer2-static/detectors/skill-frontmatter.d.ts +6 -0
  34. package/dist/layer2-static/detectors/skill-frontmatter.js +130 -0
  35. package/dist/layer2-static/engine.d.ts +2 -0
  36. package/dist/layer2-static/engine.js +0 -0
  37. package/dist/layer2-static/rule-pack-loader.js +24 -1
  38. package/dist/layer2-static/state/scan-state.d.ts +7 -2
  39. package/dist/layer2-static/state/scan-state.js +74 -30
  40. package/dist/layer2-static/text/confusables.d.ts +7 -0
  41. package/dist/layer2-static/text/confusables.js +70 -0
  42. package/dist/layer2-static/text/edit-distance.d.ts +6 -0
  43. package/dist/layer2-static/text/edit-distance.js +33 -0
  44. package/dist/layer2-static/text/encoded-payloads.d.ts +16 -0
  45. package/dist/layer2-static/text/encoded-payloads.js +116 -0
  46. package/dist/layer2-static/text/normalize.d.ts +11 -0
  47. package/dist/layer2-static/text/normalize.js +19 -0
  48. package/dist/layer2-static/text/override-phrases.d.ts +14 -0
  49. package/dist/layer2-static/text/override-phrases.js +49 -0
  50. package/dist/layer2-static/text/threat-patterns.d.ts +33 -0
  51. package/dist/layer2-static/text/threat-patterns.js +45 -0
  52. package/dist/layer2-static/text/unicode.d.ts +33 -0
  53. package/dist/layer2-static/text/unicode.js +83 -0
  54. package/dist/layer3-dynamic/deep-resource-executor.d.ts +21 -0
  55. package/dist/layer3-dynamic/deep-resource-executor.js +73 -0
  56. package/dist/layer3-dynamic/meta-agent.js +2 -1
  57. package/dist/layer3-dynamic/registry-client.d.ts +26 -0
  58. package/dist/layer3-dynamic/registry-client.js +138 -0
  59. package/dist/layer3-dynamic/registry-findings.d.ts +7 -0
  60. package/dist/layer3-dynamic/registry-findings.js +64 -0
  61. package/dist/layer3-dynamic/tool-description-scanner.js +22 -13
  62. package/dist/layer3-dynamic/toxic-flow.d.ts +4 -0
  63. package/dist/layer3-dynamic/toxic-flow.js +41 -8
  64. package/dist/pipeline.d.ts +5 -2
  65. package/dist/pipeline.js +53 -16
  66. package/dist/report-summary.d.ts +1 -1
  67. package/dist/report-summary.js +9 -1
  68. package/dist/scan.d.ts +13 -0
  69. package/dist/scan.js +291 -17
  70. package/dist/types/finding.d.ts +14 -0
  71. package/dist/types/finding.js +14 -0
  72. package/dist/types/report.d.ts +2 -0
  73. package/dist/wrapper.js +2 -19
  74. package/package.json +1 -1
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;
package/dist/config.js CHANGED
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join, resolve } from "node:path";
4
4
  import { parse as parseJsonc } from "jsonc-parser";
5
+ import { isTrustedDirectory } from "./config/trust.js";
5
6
  import { applyReportSummary, computeExitCode as computeReportExitCode } from "./report-summary.js";
6
7
  import { applySuppressionPolicy, } from "./config/suppression-policy.js";
7
8
  export const OUTPUT_FORMATS = ["terminal", "json", "sarif", "markdown", "html"];
@@ -37,6 +38,7 @@ export const DEFAULT_CONFIG = {
37
38
  known_safe_hooks: [],
38
39
  unicode_analysis: true,
39
40
  check_ide_settings: true,
41
+ first_scan_review: true,
40
42
  owasp_mapping: true,
41
43
  trusted_api_domains: [],
42
44
  rule_pack_paths: [],
@@ -47,6 +49,7 @@ export const DEFAULT_CONFIG = {
47
49
  persona: "regular",
48
50
  runtime_mode: "offline",
49
51
  workflow_audits: { enabled: false },
52
+ registry_heuristics: { recent_publish_days: 30 },
50
53
  suppress_findings: [],
51
54
  suppression_rules: [],
52
55
  layer3_remote_fetch_timeout_ms: 5000,
@@ -270,12 +273,47 @@ function pickFirst(...values) {
270
273
  }
271
274
  return undefined;
272
275
  }
276
+ // Presentation-only keys the scan target's .codegate.json may always set.
277
+ // Every other key is policy: it changes what is detected, suppressed,
278
+ // executed, or gated, so untrusted targets must not control it.
279
+ export const PROJECT_COSMETIC_KEYS = ["output_format", "tui", "owasp_mapping"];
280
+ // Keys never honored from project config, whether the target is trusted or not.
281
+ export const PROJECT_FORBIDDEN_KEYS = ["trusted_directories"];
282
+ function isProjectCosmeticKey(key) {
283
+ return PROJECT_COSMETIC_KEYS.includes(key);
284
+ }
285
+ function isProjectForbiddenKey(key) {
286
+ return PROJECT_FORBIDDEN_KEYS.includes(key);
287
+ }
288
+ function isProjectKeyHonored(key, trusted) {
289
+ if (isProjectForbiddenKey(key)) {
290
+ return false;
291
+ }
292
+ return trusted || isProjectCosmeticKey(key);
293
+ }
294
+ function partitionProjectConfig(projectConfig, trusted) {
295
+ const effective = {};
296
+ const ignoredKeys = [];
297
+ for (const [key, value] of Object.entries(projectConfig)) {
298
+ if (value === undefined) {
299
+ continue;
300
+ }
301
+ if (isProjectKeyHonored(key, trusted)) {
302
+ effective[key] = value;
303
+ continue;
304
+ }
305
+ ignoredKeys.push(key);
306
+ }
307
+ return { effective, ignoredKeys: ignoredKeys.sort() };
308
+ }
273
309
  export function resolveEffectiveConfig(options) {
274
310
  const home = options.homeDir ?? homedir();
275
311
  const scanTarget = resolve(options.scanTarget);
276
312
  const globalConfigPath = options.cli?.configPath ?? join(home, ".codegate", "config.json");
277
313
  const globalConfig = readConfigFile(globalConfigPath);
278
- const projectConfig = readConfigFile(join(scanTarget, ".codegate.json"));
314
+ const rawProjectConfig = readConfigFile(join(scanTarget, ".codegate.json"));
315
+ const projectConfigTrusted = isTrustedDirectory(scanTarget, unique([DEFAULT_CONFIG.trusted_directories, globalConfig.trusted_directories]));
316
+ const { effective: projectConfig, ignoredKeys: ignoredProjectSettings } = partitionProjectConfig(rawProjectConfig, projectConfigTrusted);
279
317
  const severity_threshold = pickFirst(normalizeSeverityThreshold(undefined), normalizeSeverityThreshold(projectConfig.severity_threshold), normalizeSeverityThreshold(globalConfig.severity_threshold), DEFAULT_CONFIG.severity_threshold) ?? DEFAULT_CONFIG.severity_threshold;
280
318
  const output_format = pickFirst(options.cli?.format, normalizeOutputFormat(projectConfig.output_format), normalizeOutputFormat(globalConfig.output_format), DEFAULT_CONFIG.output_format) ?? DEFAULT_CONFIG.output_format;
281
319
  return {
@@ -336,6 +374,7 @@ export function resolveEffectiveConfig(options) {
336
374
  ]),
337
375
  unicode_analysis: pickFirst(projectConfig.unicode_analysis, globalConfig.unicode_analysis, DEFAULT_CONFIG.unicode_analysis) ?? DEFAULT_CONFIG.unicode_analysis,
338
376
  check_ide_settings: pickFirst(projectConfig.check_ide_settings, globalConfig.check_ide_settings, DEFAULT_CONFIG.check_ide_settings) ?? DEFAULT_CONFIG.check_ide_settings,
377
+ first_scan_review: pickFirst(projectConfig.first_scan_review, globalConfig.first_scan_review, DEFAULT_CONFIG.first_scan_review) ?? true,
339
378
  owasp_mapping: pickFirst(projectConfig.owasp_mapping, globalConfig.owasp_mapping, DEFAULT_CONFIG.owasp_mapping) ?? DEFAULT_CONFIG.owasp_mapping,
340
379
  trusted_api_domains: unique([
341
380
  DEFAULT_CONFIG.trusted_api_domains,
@@ -366,6 +405,9 @@ export function resolveEffectiveConfig(options) {
366
405
  workflow_audits: {
367
406
  enabled: pickFirst(projectConfig.workflow_audits?.enabled, globalConfig.workflow_audits?.enabled, DEFAULT_CONFIG.workflow_audits?.enabled) ?? false,
368
407
  },
408
+ registry_heuristics: {
409
+ recent_publish_days: pickFirst(projectConfig.registry_heuristics?.recent_publish_days, globalConfig.registry_heuristics?.recent_publish_days, DEFAULT_CONFIG.registry_heuristics?.recent_publish_days) ?? 30,
410
+ },
369
411
  suppress_findings: unique([
370
412
  DEFAULT_CONFIG.suppress_findings,
371
413
  globalConfig.suppress_findings,
@@ -378,6 +420,8 @@ export function resolveEffectiveConfig(options) {
378
420
  ],
379
421
  layer3_remote_fetch_timeout_ms: pickFirst(readEnvOverride(LAYER3_REMOTE_FETCH_TIMEOUT_ENV), normalizePositiveInteger(projectConfig.layer3_remote_fetch_timeout_ms), normalizePositiveInteger(globalConfig.layer3_remote_fetch_timeout_ms), DEFAULT_CONFIG.layer3_remote_fetch_timeout_ms) ?? DEFAULT_CONFIG.layer3_remote_fetch_timeout_ms,
380
422
  layer3_remote_fetch_max_bytes: pickFirst(readEnvOverride(LAYER3_REMOTE_FETCH_MAX_BYTES_ENV), normalizePositiveInteger(projectConfig.layer3_remote_fetch_max_bytes), normalizePositiveInteger(globalConfig.layer3_remote_fetch_max_bytes), DEFAULT_CONFIG.layer3_remote_fetch_max_bytes) ?? DEFAULT_CONFIG.layer3_remote_fetch_max_bytes,
423
+ project_config_trusted: projectConfigTrusted,
424
+ ignored_project_settings: ignoredProjectSettings.length > 0 ? ignoredProjectSettings : undefined,
381
425
  };
382
426
  }
383
427
  export function computeExitCode(findings, threshold) {