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/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) {
@@ -0,0 +1,27 @@
1
+ import type { KnowledgeBaseEntry } from "../layer1-discovery/knowledge-base.js";
2
+ import type { OverridePhrase } from "../layer2-static/text/override-phrases.js";
3
+ export declare const CONTENT_BUNDLE_SCHEMA_VERSION = "1";
4
+ export declare const KNOWN_BAD_INDICATOR_KEYS: readonly ["file_sha256", "package_names", "url_patterns", "finding_fingerprints"];
5
+ export type KnownBadIndicatorKey = (typeof KNOWN_BAD_INDICATOR_KEYS)[number];
6
+ export type KnownBadIndicators = Partial<Record<KnownBadIndicatorKey, string[]>>;
7
+ export interface ContentBundle {
8
+ schema_version: string;
9
+ content_version: string;
10
+ released_at: string;
11
+ kb_schema_version?: string;
12
+ kb_entries?: KnowledgeBaseEntry[];
13
+ rules?: unknown[];
14
+ override_phrases?: OverridePhrase[];
15
+ popular_packages?: {
16
+ npm?: string[];
17
+ pypi?: string[];
18
+ };
19
+ known_bad?: KnownBadIndicators;
20
+ }
21
+ /**
22
+ * Verify the detached Ed25519 signature over the exact bundle bytes.
23
+ * Verification always happens BEFORE parsing untrusted bytes.
24
+ */
25
+ export declare function verifyBundleSignature(bundleBytes: Buffer, signatureBase64: string, publicKeyPem: string): boolean;
26
+ /** Structural validation of a verified bundle. Throws with a clear reason. */
27
+ export declare function parseContentBundle(bundleBytes: Buffer): ContentBundle;
@@ -0,0 +1,73 @@
1
+ import { createPublicKey, verify as cryptoVerify } from "node:crypto";
2
+ export const CONTENT_BUNDLE_SCHEMA_VERSION = "1";
3
+ export const KNOWN_BAD_INDICATOR_KEYS = [
4
+ "file_sha256",
5
+ "package_names",
6
+ "url_patterns",
7
+ "finding_fingerprints",
8
+ ];
9
+ function isRecord(value) {
10
+ return typeof value === "object" && value !== null && !Array.isArray(value);
11
+ }
12
+ function isStringArray(value) {
13
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
14
+ }
15
+ /**
16
+ * Verify the detached Ed25519 signature over the exact bundle bytes.
17
+ * Verification always happens BEFORE parsing untrusted bytes.
18
+ */
19
+ export function verifyBundleSignature(bundleBytes, signatureBase64, publicKeyPem) {
20
+ try {
21
+ const key = createPublicKey(publicKeyPem);
22
+ const signature = Buffer.from(signatureBase64.trim(), "base64");
23
+ return cryptoVerify(null, bundleBytes, key, signature);
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ }
29
+ /** Structural validation of a verified bundle. Throws with a clear reason. */
30
+ export function parseContentBundle(bundleBytes) {
31
+ let parsed;
32
+ try {
33
+ parsed = JSON.parse(bundleBytes.toString("utf8"));
34
+ }
35
+ catch (error) {
36
+ const reason = error instanceof Error ? error.message : String(error);
37
+ throw new Error(`Content bundle is not valid JSON: ${reason}`, { cause: error });
38
+ }
39
+ if (!isRecord(parsed)) {
40
+ throw new Error("Content bundle must be a JSON object");
41
+ }
42
+ if (parsed.schema_version !== CONTENT_BUNDLE_SCHEMA_VERSION) {
43
+ throw new Error(`Unsupported content bundle schema_version: ${String(parsed.schema_version)} ` +
44
+ `(this build supports ${CONTENT_BUNDLE_SCHEMA_VERSION})`);
45
+ }
46
+ if (typeof parsed.content_version !== "string" || parsed.content_version.length === 0) {
47
+ throw new Error("Content bundle is missing content_version");
48
+ }
49
+ if (typeof parsed.released_at !== "string" || Number.isNaN(Date.parse(parsed.released_at))) {
50
+ throw new Error("Content bundle is missing a valid released_at timestamp");
51
+ }
52
+ if (parsed.kb_entries !== undefined && !Array.isArray(parsed.kb_entries)) {
53
+ throw new Error("Content bundle kb_entries must be an array");
54
+ }
55
+ if (parsed.rules !== undefined && !Array.isArray(parsed.rules)) {
56
+ throw new Error("Content bundle rules must be an array");
57
+ }
58
+ if (parsed.override_phrases !== undefined && !Array.isArray(parsed.override_phrases)) {
59
+ throw new Error("Content bundle override_phrases must be an array");
60
+ }
61
+ if (parsed.known_bad !== undefined) {
62
+ if (!isRecord(parsed.known_bad)) {
63
+ throw new Error("Content bundle known_bad must be an object");
64
+ }
65
+ for (const key of KNOWN_BAD_INDICATOR_KEYS) {
66
+ const list = parsed.known_bad[key];
67
+ if (list !== undefined && !isStringArray(list)) {
68
+ throw new Error(`Content bundle known_bad.${key} must be an array of strings`);
69
+ }
70
+ }
71
+ }
72
+ return parsed;
73
+ }
@@ -0,0 +1,27 @@
1
+ import { type ContentBundle } from "./content-bundle.js";
2
+ export interface ContentStoreDeps {
3
+ homeDir?: () => string;
4
+ publisherKeyPem?: string | null;
5
+ }
6
+ export interface InstalledContentVersion {
7
+ version: string;
8
+ releasedAt: string;
9
+ }
10
+ /** Installed versions, newest first by released_at. */
11
+ export declare function listInstalledContentVersions(deps?: ContentStoreDeps): InstalledContentVersion[];
12
+ export declare function getActiveContentVersion(deps?: ContentStoreDeps): string | null;
13
+ export declare function setActiveContentVersion(version: string, deps?: ContentStoreDeps): void;
14
+ export interface StoredContentVersion {
15
+ version: string;
16
+ pruned: string[];
17
+ }
18
+ /** Persist a verified bundle, activate it, and prune old versions. */
19
+ export declare function storeContentVersion(bundle: ContentBundle, bundleBytes: Buffer, signatureBase64: string, deps?: ContentStoreDeps): StoredContentVersion;
20
+ export declare function resetContentStoreCache(): void;
21
+ /**
22
+ * Load the active, signature-verified content bundle, or null when no
23
+ * publisher key is configured, nothing is installed, or verification or
24
+ * parsing fails. Never throws: content-feed problems must degrade to
25
+ * bundled content, not break scanning.
26
+ */
27
+ export declare function loadActiveContentBundle(deps?: ContentStoreDeps): ContentBundle | null;
Binary file
@@ -0,0 +1,32 @@
1
+ import { type ContentStoreDeps } from "./content-store.js";
2
+ /**
3
+ * Explicit, user-initiated content updates. Scanning never triggers a
4
+ * fetch; these functions run only from the update-kb/update-rules commands.
5
+ * Integrity comes from the Ed25519 signature (verified before parsing),
6
+ * not from the transport, so release-hosting redirects are acceptable —
7
+ * but the URL must be https.
8
+ */
9
+ export declare const DEFAULT_CONTENT_BASE_URL = "https://github.com/jonathansantilli/codegate-content/releases/latest/download";
10
+ export interface ContentUpdaterDeps extends ContentStoreDeps {
11
+ fetchImpl?: typeof fetch;
12
+ }
13
+ export interface ContentUpdateOptions {
14
+ baseUrl?: string;
15
+ }
16
+ export interface ContentUpdateResult {
17
+ changed: boolean;
18
+ previousVersion: string | null;
19
+ version: string;
20
+ pruned: string[];
21
+ }
22
+ export declare function updateContent(deps?: ContentUpdaterDeps, options?: ContentUpdateOptions): Promise<ContentUpdateResult>;
23
+ export interface ContentCheckResult {
24
+ currentVersion: string | null;
25
+ remoteVersion: string;
26
+ updateAvailable: boolean;
27
+ }
28
+ export declare function checkContentUpdate(deps?: ContentUpdaterDeps, options?: ContentUpdateOptions): Promise<ContentCheckResult>;
29
+ export interface ContentRollbackResult {
30
+ version: string;
31
+ }
32
+ export declare function rollbackContent(deps?: ContentUpdaterDeps): ContentRollbackResult;
@@ -0,0 +1,111 @@
1
+ import { parseContentBundle, verifyBundleSignature } from "./content-bundle.js";
2
+ import { getActiveContentVersion, listInstalledContentVersions, setActiveContentVersion, storeContentVersion, } from "./content-store.js";
3
+ import { CONTENT_PUBLISHER_PUBLIC_KEY_PEM } from "./publisher-key.js";
4
+ /**
5
+ * Explicit, user-initiated content updates. Scanning never triggers a
6
+ * fetch; these functions run only from the update-kb/update-rules commands.
7
+ * Integrity comes from the Ed25519 signature (verified before parsing),
8
+ * not from the transport, so release-hosting redirects are acceptable —
9
+ * but the URL must be https.
10
+ */
11
+ export const DEFAULT_CONTENT_BASE_URL = "https://github.com/jonathansantilli/codegate-content/releases/latest/download";
12
+ const BUNDLE_NAME = "codegate-content.json";
13
+ const FETCH_TIMEOUT_MS = 15_000;
14
+ const MAX_BUNDLE_BYTES = 8 * 1024 * 1024;
15
+ const MAX_SIGNATURE_BYTES = 4096;
16
+ function requirePublisherKey(deps) {
17
+ const key = deps.publisherKeyPem !== undefined ? deps.publisherKeyPem : CONTENT_PUBLISHER_PUBLIC_KEY_PEM;
18
+ if (!key) {
19
+ throw new Error("No content publisher key is configured in this build, so remote content cannot be " +
20
+ "verified. Content updates ship with CodeGate releases: npm update -g codegate-ai");
21
+ }
22
+ return key;
23
+ }
24
+ async function fetchBytes(url, maxBytes, deps) {
25
+ const parsed = new URL(url);
26
+ if (parsed.protocol !== "https:") {
27
+ throw new Error(`Content URL must use https: ${url}`);
28
+ }
29
+ const fetchImpl = deps.fetchImpl ?? fetch;
30
+ const response = await fetchImpl(url, {
31
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
32
+ });
33
+ if (!response.ok) {
34
+ throw new Error(`Content download failed with HTTP ${response.status} for ${url}`);
35
+ }
36
+ const contentLength = response.headers.get("content-length");
37
+ if (contentLength && Number.parseInt(contentLength, 10) > maxBytes) {
38
+ throw new Error(`Content download exceeds ${maxBytes} bytes: ${url}`);
39
+ }
40
+ const body = response.body;
41
+ if (!body) {
42
+ const buffer = Buffer.from(await response.arrayBuffer());
43
+ if (buffer.byteLength > maxBytes) {
44
+ throw new Error(`Content download exceeds ${maxBytes} bytes: ${url}`);
45
+ }
46
+ return buffer;
47
+ }
48
+ const reader = body.getReader();
49
+ const chunks = [];
50
+ let total = 0;
51
+ for (;;) {
52
+ const { done, value } = await reader.read();
53
+ if (done) {
54
+ break;
55
+ }
56
+ total += value.byteLength;
57
+ if (total > maxBytes) {
58
+ await reader.cancel();
59
+ throw new Error(`Content download exceeds ${maxBytes} bytes: ${url}`);
60
+ }
61
+ chunks.push(value);
62
+ }
63
+ return Buffer.concat(chunks);
64
+ }
65
+ async function fetchAndVerifyBundle(deps, options) {
66
+ const key = requirePublisherKey(deps);
67
+ const baseUrl = (options.baseUrl ?? DEFAULT_CONTENT_BASE_URL).replace(/\/+$/u, "");
68
+ const bundleBytes = await fetchBytes(`${baseUrl}/${BUNDLE_NAME}`, MAX_BUNDLE_BYTES, deps);
69
+ const signature = (await fetchBytes(`${baseUrl}/${BUNDLE_NAME}.sig`, MAX_SIGNATURE_BYTES, deps)).toString("utf8");
70
+ if (!verifyBundleSignature(bundleBytes, signature, key)) {
71
+ throw new Error("Content bundle signature verification failed. The download was rejected and nothing was installed.");
72
+ }
73
+ // Parse only after the signature proved the bytes came from the publisher.
74
+ const bundle = parseContentBundle(bundleBytes);
75
+ return { bundleBytes, signature, version: bundle.content_version };
76
+ }
77
+ export async function updateContent(deps = {}, options = {}) {
78
+ const fetched = await fetchAndVerifyBundle(deps, options);
79
+ const previousVersion = getActiveContentVersion(deps);
80
+ if (previousVersion === fetched.version) {
81
+ return { changed: false, previousVersion, version: fetched.version, pruned: [] };
82
+ }
83
+ const bundle = parseContentBundle(fetched.bundleBytes);
84
+ const stored = storeContentVersion(bundle, fetched.bundleBytes, fetched.signature, deps);
85
+ return {
86
+ changed: true,
87
+ previousVersion,
88
+ version: stored.version,
89
+ pruned: stored.pruned,
90
+ };
91
+ }
92
+ export async function checkContentUpdate(deps = {}, options = {}) {
93
+ const fetched = await fetchAndVerifyBundle(deps, options);
94
+ const currentVersion = getActiveContentVersion(deps);
95
+ return {
96
+ currentVersion,
97
+ remoteVersion: fetched.version,
98
+ updateAvailable: currentVersion !== fetched.version,
99
+ };
100
+ }
101
+ export function rollbackContent(deps = {}) {
102
+ const installed = listInstalledContentVersions(deps);
103
+ const active = getActiveContentVersion(deps);
104
+ const candidates = installed.filter((entry) => entry.version !== active);
105
+ const target = candidates[0];
106
+ if (!target) {
107
+ throw new Error("No previous content version is installed to roll back to.");
108
+ }
109
+ setActiveContentVersion(target.version, deps);
110
+ return { version: target.version };
111
+ }
@@ -0,0 +1,26 @@
1
+ export interface KnownBadDeps {
2
+ homeDir?: () => string;
3
+ }
4
+ /**
5
+ * Indicators normalized for matching: hashes and package names lowercased,
6
+ * fingerprints carrying the `sha256:` prefix used by finding fingerprints.
7
+ */
8
+ export interface ResolvedKnownBadIndicators {
9
+ fileSha256: ReadonlySet<string>;
10
+ packageNames: ReadonlySet<string>;
11
+ urlPatterns: readonly string[];
12
+ findingFingerprints: ReadonlySet<string>;
13
+ }
14
+ export declare function hasKnownBadIndicators(indicators: ResolvedKnownBadIndicators): boolean;
15
+ /** Accepts `sha256:<hex>` or bare hex; returns bare lowercase hex or null. */
16
+ export declare function normalizeSha256Indicator(value: string): string | null;
17
+ /** Finding fingerprints are stored and compared with the `sha256:` prefix. */
18
+ export declare function normalizeFingerprintIndicator(value: string): string | null;
19
+ export declare function resetKnownBadIndicatorsCache(): void;
20
+ /**
21
+ * Known-bad indicators merged from the verified content feed and the local
22
+ * `~/.codegate/known-bad.json`. The local file is user-controlled (same trust
23
+ * as the global config) and exists so indicators work before the feed is
24
+ * live. Never throws: indicator problems must not break scanning.
25
+ */
26
+ export declare function loadKnownBadIndicators(deps?: KnownBadDeps): ResolvedKnownBadIndicators;
@@ -0,0 +1,100 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { KNOWN_BAD_INDICATOR_KEYS, } from "./content-bundle.js";
5
+ import { loadActiveContentBundle } from "./content-store.js";
6
+ const LOCAL_INDICATORS_FILE = "known-bad.json";
7
+ const SHA256_HEX_PATTERN = /^[0-9a-f]{64}$/u;
8
+ const SHA256_PREFIX = "sha256:";
9
+ export function hasKnownBadIndicators(indicators) {
10
+ return (indicators.fileSha256.size > 0 ||
11
+ indicators.packageNames.size > 0 ||
12
+ indicators.urlPatterns.length > 0 ||
13
+ indicators.findingFingerprints.size > 0);
14
+ }
15
+ /** Accepts `sha256:<hex>` or bare hex; returns bare lowercase hex or null. */
16
+ export function normalizeSha256Indicator(value) {
17
+ const trimmed = value.trim().toLowerCase();
18
+ const bare = trimmed.startsWith(SHA256_PREFIX) ? trimmed.slice(SHA256_PREFIX.length) : trimmed;
19
+ return SHA256_HEX_PATTERN.test(bare) ? bare : null;
20
+ }
21
+ /** Finding fingerprints are stored and compared with the `sha256:` prefix. */
22
+ export function normalizeFingerprintIndicator(value) {
23
+ const bare = normalizeSha256Indicator(value);
24
+ return bare === null ? null : `${SHA256_PREFIX}${bare}`;
25
+ }
26
+ function isRecord(value) {
27
+ return typeof value === "object" && value !== null && !Array.isArray(value);
28
+ }
29
+ /** Keep only well-formed string lists; anything else degrades to empty. */
30
+ function sanitizeIndicators(value) {
31
+ if (!isRecord(value)) {
32
+ return {};
33
+ }
34
+ const sanitized = {};
35
+ for (const key of KNOWN_BAD_INDICATOR_KEYS) {
36
+ const list = value[key];
37
+ if (Array.isArray(list)) {
38
+ sanitized[key] = list.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
39
+ }
40
+ }
41
+ return sanitized;
42
+ }
43
+ function loadLocalIndicators(deps) {
44
+ const home = deps.homeDir ? deps.homeDir() : homedir();
45
+ try {
46
+ const raw = readFileSync(join(home, ".codegate", LOCAL_INDICATORS_FILE), "utf8");
47
+ return sanitizeIndicators(JSON.parse(raw));
48
+ }
49
+ catch {
50
+ return {};
51
+ }
52
+ }
53
+ function indicatorList(sources, key) {
54
+ return sources.flatMap((source) => source[key] ?? []);
55
+ }
56
+ function resolveIndicators(sources) {
57
+ const fileSha256 = new Set();
58
+ for (const entry of indicatorList(sources, "file_sha256")) {
59
+ const normalized = normalizeSha256Indicator(entry);
60
+ if (normalized) {
61
+ fileSha256.add(normalized);
62
+ }
63
+ }
64
+ const packageNames = new Set(indicatorList(sources, "package_names").map((entry) => entry.trim().toLowerCase()));
65
+ const urlPatterns = Array.from(new Set(indicatorList(sources, "url_patterns").map((entry) => entry.trim().toLowerCase())));
66
+ const findingFingerprints = new Set();
67
+ for (const entry of indicatorList(sources, "finding_fingerprints")) {
68
+ const normalized = normalizeFingerprintIndicator(entry);
69
+ if (normalized) {
70
+ findingFingerprints.add(normalized);
71
+ }
72
+ }
73
+ return { fileSha256, packageNames, urlPatterns, findingFingerprints };
74
+ }
75
+ let cache = null;
76
+ export function resetKnownBadIndicatorsCache() {
77
+ cache = null;
78
+ }
79
+ /**
80
+ * Known-bad indicators merged from the verified content feed and the local
81
+ * `~/.codegate/known-bad.json`. The local file is user-controlled (same trust
82
+ * as the global config) and exists so indicators work before the feed is
83
+ * live. Never throws: indicator problems must not break scanning.
84
+ */
85
+ export function loadKnownBadIndicators(deps = {}) {
86
+ const home = deps.homeDir ? deps.homeDir() : homedir();
87
+ if (cache && cache.key === home) {
88
+ return cache.indicators;
89
+ }
90
+ let feedIndicators;
91
+ try {
92
+ feedIndicators = sanitizeIndicators(loadActiveContentBundle()?.known_bad);
93
+ }
94
+ catch {
95
+ feedIndicators = {};
96
+ }
97
+ const indicators = resolveIndicators([feedIndicators, loadLocalIndicators(deps)]);
98
+ cache = { key: home, indicators };
99
+ return indicators;
100
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Ed25519 public key of the CodeGate content publisher, PEM (SPKI) format.
3
+ *
4
+ * Deliberately null until the owner decides key custody and the content
5
+ * repository location (see docs/content-feed.md). While null, the update
6
+ * commands refuse to fetch and every loader uses only bundled content.
7
+ * Generate a pair with scripts/content-feed/generate-keypair.mjs and paste
8
+ * the public PEM here.
9
+ */
10
+ export declare const CONTENT_PUBLISHER_PUBLIC_KEY_PEM: string | null;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Ed25519 public key of the CodeGate content publisher, PEM (SPKI) format.
3
+ *
4
+ * Deliberately null until the owner decides key custody and the content
5
+ * repository location (see docs/content-feed.md). While null, the update
6
+ * commands refuse to fetch and every loader uses only bundled content.
7
+ * Generate a pair with scripts/content-feed/generate-keypair.mjs and paste
8
+ * the public PEM here.
9
+ */
10
+ export const CONTENT_PUBLISHER_PUBLIC_KEY_PEM = null;
@@ -2,6 +2,7 @@ import { readdirSync, readFileSync } from "node:fs";
2
2
  import { createRequire } from "node:module";
3
3
  import { dirname, extname, join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { loadActiveContentBundle } from "../content/content-store.js";
5
6
  const defaultKnowledgeBaseDir = resolve(dirname(fileURLToPath(import.meta.url)), "../knowledge-base");
6
7
  const schemaPath = join(defaultKnowledgeBaseDir, "schema.json");
7
8
  const require = createRequire(import.meta.url);
@@ -32,7 +33,38 @@ export function validateKnowledgeBaseEntry(candidate, path = schemaPath) {
32
33
  errors: toErrors(validator.errors),
33
34
  };
34
35
  }
35
- export function loadKnowledgeBase(baseDir = defaultKnowledgeBaseDir) {
36
+ function loadKnowledgeBaseFromContentFeed() {
37
+ try {
38
+ const bundle = loadActiveContentBundle();
39
+ if (!bundle?.kb_entries || bundle.kb_entries.length === 0) {
40
+ return null;
41
+ }
42
+ const validator = createValidator(schemaPath);
43
+ for (const entry of bundle.kb_entries) {
44
+ if (!validator(entry)) {
45
+ // One invalid feed entry disqualifies the whole feed KB; bundled
46
+ // content is the safe fallback.
47
+ return null;
48
+ }
49
+ }
50
+ return {
51
+ schemaVersion: bundle.kb_schema_version ?? `feed-${bundle.content_version}`,
52
+ entries: bundle.kb_entries,
53
+ };
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ }
59
+ export function loadKnowledgeBase(baseDir) {
60
+ // An explicit baseDir bypasses the content feed (tests, custom setups).
61
+ if (baseDir === undefined) {
62
+ const fromFeed = loadKnowledgeBaseFromContentFeed();
63
+ if (fromFeed) {
64
+ return fromFeed;
65
+ }
66
+ baseDir = defaultKnowledgeBaseDir;
67
+ }
36
68
  const schema = loadSchema(join(baseDir, "schema.json"));
37
69
  const validator = createValidator(join(baseDir, "schema.json"));
38
70
  const entryFiles = readdirSync(baseDir)
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Well-known MCP server packages used as the reference set for typosquat
3
+ * detection. Curated, not exhaustive: entries should be packages an
4
+ * attacker would plausibly imitate. Data-driven so the content feed can
5
+ * extend it without a release.
6
+ */
7
+ export declare const POPULAR_MCP_PACKAGES: Readonly<{
8
+ npm: readonly string[];
9
+ pypi: readonly string[];
10
+ }>;
11
+ export declare function resetActivePopularMcpPackagesCache(): void;
12
+ /** Bundled popular packages plus any extras delivered by the verified content feed. */
13
+ export declare function activePopularMcpPackages(): {
14
+ npm: readonly string[];
15
+ pypi: readonly string[];
16
+ };