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
@@ -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
+ };
@@ -0,0 +1,83 @@
1
+ import { loadActiveContentBundle } from "../../content/content-store.js";
2
+ /**
3
+ * Well-known MCP server packages used as the reference set for typosquat
4
+ * detection. Curated, not exhaustive: entries should be packages an
5
+ * attacker would plausibly imitate. Data-driven so the content feed can
6
+ * extend it without a release.
7
+ */
8
+ export const POPULAR_MCP_PACKAGES = {
9
+ npm: [
10
+ "@modelcontextprotocol/server-brave-search",
11
+ "@modelcontextprotocol/server-everything",
12
+ "@modelcontextprotocol/server-filesystem",
13
+ "@modelcontextprotocol/server-github",
14
+ "@modelcontextprotocol/server-gitlab",
15
+ "@modelcontextprotocol/server-google-maps",
16
+ "@modelcontextprotocol/server-memory",
17
+ "@modelcontextprotocol/server-postgres",
18
+ "@modelcontextprotocol/server-puppeteer",
19
+ "@modelcontextprotocol/server-sequential-thinking",
20
+ "@modelcontextprotocol/server-slack",
21
+ "@modelcontextprotocol/server-sqlite",
22
+ "@modelcontextprotocol/inspector",
23
+ "@anthropic/mcp-server-filesystem",
24
+ "@playwright/mcp",
25
+ "@browserbasehq/mcp",
26
+ "@notionhq/notion-mcp-server",
27
+ "@supabase/mcp-server-supabase",
28
+ "@upstash/context7-mcp",
29
+ "@21st-dev/magic",
30
+ "@sentry/mcp-server",
31
+ "@elastic/mcp-server-elasticsearch",
32
+ "firecrawl-mcp",
33
+ "tavily-mcp",
34
+ "exa-mcp-server",
35
+ "mcp-remote",
36
+ "supergateway",
37
+ "figma-developer-mcp",
38
+ "chrome-devtools-mcp",
39
+ "graphlit-mcp-server",
40
+ ],
41
+ pypi: [
42
+ "mcp",
43
+ "mcp-server-git",
44
+ "mcp-server-fetch",
45
+ "mcp-server-time",
46
+ "mcp-server-sqlite",
47
+ "mcp-server-sentry",
48
+ "fastmcp",
49
+ "mcp-atlassian",
50
+ "awslabs.aws-documentation-mcp-server",
51
+ ],
52
+ };
53
+ let cachedActivePackages = null;
54
+ export function resetActivePopularMcpPackagesCache() {
55
+ cachedActivePackages = null;
56
+ }
57
+ function feedList(value) {
58
+ return Array.isArray(value)
59
+ ? value.filter((entry) => typeof entry === "string" && entry.length > 0)
60
+ : [];
61
+ }
62
+ /** Bundled popular packages plus any extras delivered by the verified content feed. */
63
+ export function activePopularMcpPackages() {
64
+ if (cachedActivePackages) {
65
+ return cachedActivePackages;
66
+ }
67
+ let npmExtra;
68
+ let pypiExtra;
69
+ try {
70
+ const bundle = loadActiveContentBundle();
71
+ npmExtra = feedList(bundle?.popular_packages?.npm);
72
+ pypiExtra = feedList(bundle?.popular_packages?.pypi);
73
+ }
74
+ catch {
75
+ npmExtra = [];
76
+ pypiExtra = [];
77
+ }
78
+ cachedActivePackages = {
79
+ npm: [...new Set([...POPULAR_MCP_PACKAGES.npm, ...npmExtra])],
80
+ pypi: [...new Set([...POPULAR_MCP_PACKAGES.pypi, ...pypiExtra])],
81
+ };
82
+ return cachedActivePackages;
83
+ }
@@ -0,0 +1,22 @@
1
+ import { type ResolvedKnownBadIndicators } from "../../content/known-bad.js";
2
+ import type { Finding } from "../../types/finding.js";
3
+ export interface KnownBadDetectionInput {
4
+ filePath: string;
5
+ parsed: unknown;
6
+ textContent: string;
7
+ indicators: ResolvedKnownBadIndicators;
8
+ }
9
+ export declare const KNOWN_BAD_MATCH_KIND: {
10
+ readonly FileHash: "file-hash";
11
+ readonly PackageName: "package-name";
12
+ readonly UrlPattern: "url-pattern";
13
+ };
14
+ export type KnownBadMatchKind = (typeof KNOWN_BAD_MATCH_KIND)[keyof typeof KNOWN_BAD_MATCH_KIND];
15
+ export declare const KNOWN_BAD_RULE_ID = "known-malicious-content";
16
+ export declare function detectKnownBadContent(input: KnownBadDetectionInput): Finding[];
17
+ /**
18
+ * Escalate findings whose stable fingerprint appears in the known-bad
19
+ * indicator set to CRITICAL. Runs on the assembled finding list so it also
20
+ * covers findings produced outside the static engine.
21
+ */
22
+ export declare function escalateKnownBadFindings(findings: Finding[], indicators: ResolvedKnownBadIndicators): Finding[];