@kairosinternational/watchman 0.1.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 (159) hide show
  1. package/PHASE_3A_COMPLETE.md +63 -0
  2. package/dist/engine.d.ts +9 -0
  3. package/dist/engine.d.ts.map +1 -0
  4. package/dist/engine.js +99 -0
  5. package/dist/engine.js.map +1 -0
  6. package/dist/index.d.ts +12 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +21 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/scanners/ai-tool-integrity/index.d.ts +2 -0
  11. package/dist/scanners/ai-tool-integrity/index.d.ts.map +1 -0
  12. package/dist/scanners/ai-tool-integrity/index.js +6 -0
  13. package/dist/scanners/ai-tool-integrity/index.js.map +1 -0
  14. package/dist/scanners/ai-tool-integrity/patterns/injection-signatures.d.ts +7 -0
  15. package/dist/scanners/ai-tool-integrity/patterns/injection-signatures.d.ts.map +1 -0
  16. package/dist/scanners/ai-tool-integrity/patterns/injection-signatures.js +51 -0
  17. package/dist/scanners/ai-tool-integrity/patterns/injection-signatures.js.map +1 -0
  18. package/dist/scanners/ai-tool-integrity/rules/prompt-injection.rule.d.ts +3 -0
  19. package/dist/scanners/ai-tool-integrity/rules/prompt-injection.rule.d.ts.map +1 -0
  20. package/dist/scanners/ai-tool-integrity/rules/prompt-injection.rule.js +49 -0
  21. package/dist/scanners/ai-tool-integrity/rules/prompt-injection.rule.js.map +1 -0
  22. package/dist/scanners/ai-tool-integrity/rules/tool-manifest-drift.rule.d.ts +3 -0
  23. package/dist/scanners/ai-tool-integrity/rules/tool-manifest-drift.rule.d.ts.map +1 -0
  24. package/dist/scanners/ai-tool-integrity/rules/tool-manifest-drift.rule.js +71 -0
  25. package/dist/scanners/ai-tool-integrity/rules/tool-manifest-drift.rule.js.map +1 -0
  26. package/dist/scanners/ai-tool-integrity/rules/unauthorized-model-call.rule.d.ts +3 -0
  27. package/dist/scanners/ai-tool-integrity/rules/unauthorized-model-call.rule.d.ts.map +1 -0
  28. package/dist/scanners/ai-tool-integrity/rules/unauthorized-model-call.rule.js +56 -0
  29. package/dist/scanners/ai-tool-integrity/rules/unauthorized-model-call.rule.js.map +1 -0
  30. package/dist/scanners/ai-tool-integrity/scanner.d.ts +3 -0
  31. package/dist/scanners/ai-tool-integrity/scanner.d.ts.map +1 -0
  32. package/dist/scanners/ai-tool-integrity/scanner.js +26 -0
  33. package/dist/scanners/ai-tool-integrity/scanner.js.map +1 -0
  34. package/dist/scanners/dependency-integrity/index.d.ts +2 -0
  35. package/dist/scanners/dependency-integrity/index.d.ts.map +1 -0
  36. package/dist/scanners/dependency-integrity/index.js +6 -0
  37. package/dist/scanners/dependency-integrity/index.js.map +1 -0
  38. package/dist/scanners/dependency-integrity/patterns/known-typosquats.d.ts +8 -0
  39. package/dist/scanners/dependency-integrity/patterns/known-typosquats.d.ts.map +1 -0
  40. package/dist/scanners/dependency-integrity/patterns/known-typosquats.js +38 -0
  41. package/dist/scanners/dependency-integrity/patterns/known-typosquats.js.map +1 -0
  42. package/dist/scanners/dependency-integrity/rules/hash-validation.rule.d.ts +3 -0
  43. package/dist/scanners/dependency-integrity/rules/hash-validation.rule.d.ts.map +1 -0
  44. package/dist/scanners/dependency-integrity/rules/hash-validation.rule.js +64 -0
  45. package/dist/scanners/dependency-integrity/rules/hash-validation.rule.js.map +1 -0
  46. package/dist/scanners/dependency-integrity/rules/transitive-drift.rule.d.ts +3 -0
  47. package/dist/scanners/dependency-integrity/rules/transitive-drift.rule.d.ts.map +1 -0
  48. package/dist/scanners/dependency-integrity/rules/transitive-drift.rule.js +69 -0
  49. package/dist/scanners/dependency-integrity/rules/transitive-drift.rule.js.map +1 -0
  50. package/dist/scanners/dependency-integrity/rules/typosquat-check.rule.d.ts +3 -0
  51. package/dist/scanners/dependency-integrity/rules/typosquat-check.rule.d.ts.map +1 -0
  52. package/dist/scanners/dependency-integrity/rules/typosquat-check.rule.js +94 -0
  53. package/dist/scanners/dependency-integrity/rules/typosquat-check.rule.js.map +1 -0
  54. package/dist/scanners/dependency-integrity/scanner.d.ts +3 -0
  55. package/dist/scanners/dependency-integrity/scanner.d.ts.map +1 -0
  56. package/dist/scanners/dependency-integrity/scanner.js +26 -0
  57. package/dist/scanners/dependency-integrity/scanner.js.map +1 -0
  58. package/dist/scanners/runtime-monitor/index.d.ts +2 -0
  59. package/dist/scanners/runtime-monitor/index.d.ts.map +1 -0
  60. package/dist/scanners/runtime-monitor/index.js +6 -0
  61. package/dist/scanners/runtime-monitor/index.js.map +1 -0
  62. package/dist/scanners/runtime-monitor/patterns/known-bad-destinations.d.ts +8 -0
  63. package/dist/scanners/runtime-monitor/patterns/known-bad-destinations.d.ts.map +1 -0
  64. package/dist/scanners/runtime-monitor/patterns/known-bad-destinations.js +52 -0
  65. package/dist/scanners/runtime-monitor/patterns/known-bad-destinations.js.map +1 -0
  66. package/dist/scanners/runtime-monitor/rules/filesystem-access.rule.d.ts +3 -0
  67. package/dist/scanners/runtime-monitor/rules/filesystem-access.rule.d.ts.map +1 -0
  68. package/dist/scanners/runtime-monitor/rules/filesystem-access.rule.js +68 -0
  69. package/dist/scanners/runtime-monitor/rules/filesystem-access.rule.js.map +1 -0
  70. package/dist/scanners/runtime-monitor/rules/outbound-network.rule.d.ts +3 -0
  71. package/dist/scanners/runtime-monitor/rules/outbound-network.rule.d.ts.map +1 -0
  72. package/dist/scanners/runtime-monitor/rules/outbound-network.rule.js +62 -0
  73. package/dist/scanners/runtime-monitor/rules/outbound-network.rule.js.map +1 -0
  74. package/dist/scanners/runtime-monitor/rules/process-spawn.rule.d.ts +3 -0
  75. package/dist/scanners/runtime-monitor/rules/process-spawn.rule.d.ts.map +1 -0
  76. package/dist/scanners/runtime-monitor/rules/process-spawn.rule.js +55 -0
  77. package/dist/scanners/runtime-monitor/rules/process-spawn.rule.js.map +1 -0
  78. package/dist/scanners/runtime-monitor/rules/resource-anomaly.rule.d.ts +3 -0
  79. package/dist/scanners/runtime-monitor/rules/resource-anomaly.rule.d.ts.map +1 -0
  80. package/dist/scanners/runtime-monitor/rules/resource-anomaly.rule.js +76 -0
  81. package/dist/scanners/runtime-monitor/rules/resource-anomaly.rule.js.map +1 -0
  82. package/dist/scanners/runtime-monitor/scanner.d.ts +3 -0
  83. package/dist/scanners/runtime-monitor/scanner.d.ts.map +1 -0
  84. package/dist/scanners/runtime-monitor/scanner.js +28 -0
  85. package/dist/scanners/runtime-monitor/scanner.js.map +1 -0
  86. package/dist/scanners/secrets-exposure/index.d.ts +2 -0
  87. package/dist/scanners/secrets-exposure/index.d.ts.map +1 -0
  88. package/dist/scanners/secrets-exposure/index.js +6 -0
  89. package/dist/scanners/secrets-exposure/index.js.map +1 -0
  90. package/dist/scanners/secrets-exposure/patterns/secret-signatures.d.ts +7 -0
  91. package/dist/scanners/secrets-exposure/patterns/secret-signatures.d.ts.map +1 -0
  92. package/dist/scanners/secrets-exposure/patterns/secret-signatures.js +76 -0
  93. package/dist/scanners/secrets-exposure/patterns/secret-signatures.js.map +1 -0
  94. package/dist/scanners/secrets-exposure/rules/entropy-check.rule.d.ts +3 -0
  95. package/dist/scanners/secrets-exposure/rules/entropy-check.rule.d.ts.map +1 -0
  96. package/dist/scanners/secrets-exposure/rules/entropy-check.rule.js +77 -0
  97. package/dist/scanners/secrets-exposure/rules/entropy-check.rule.js.map +1 -0
  98. package/dist/scanners/secrets-exposure/rules/known-patterns.rule.d.ts +3 -0
  99. package/dist/scanners/secrets-exposure/rules/known-patterns.rule.d.ts.map +1 -0
  100. package/dist/scanners/secrets-exposure/rules/known-patterns.rule.js +62 -0
  101. package/dist/scanners/secrets-exposure/rules/known-patterns.rule.js.map +1 -0
  102. package/dist/scanners/secrets-exposure/rules/response-echo.rule.d.ts +3 -0
  103. package/dist/scanners/secrets-exposure/rules/response-echo.rule.d.ts.map +1 -0
  104. package/dist/scanners/secrets-exposure/rules/response-echo.rule.js +67 -0
  105. package/dist/scanners/secrets-exposure/rules/response-echo.rule.js.map +1 -0
  106. package/dist/scanners/secrets-exposure/scanner.d.ts +3 -0
  107. package/dist/scanners/secrets-exposure/scanner.d.ts.map +1 -0
  108. package/dist/scanners/secrets-exposure/scanner.js +26 -0
  109. package/dist/scanners/secrets-exposure/scanner.js.map +1 -0
  110. package/dist/types/config.types.d.ts +22 -0
  111. package/dist/types/config.types.d.ts.map +1 -0
  112. package/dist/types/config.types.js +15 -0
  113. package/dist/types/config.types.js.map +1 -0
  114. package/dist/types/context.types.d.ts +23 -0
  115. package/dist/types/context.types.d.ts.map +1 -0
  116. package/dist/types/context.types.js +3 -0
  117. package/dist/types/context.types.js.map +1 -0
  118. package/dist/types/finding.types.d.ts +33 -0
  119. package/dist/types/finding.types.d.ts.map +1 -0
  120. package/dist/types/finding.types.js +3 -0
  121. package/dist/types/finding.types.js.map +1 -0
  122. package/dist/types/index.d.ts +5 -0
  123. package/dist/types/index.d.ts.map +1 -0
  124. package/dist/types/index.js +6 -0
  125. package/dist/types/index.js.map +1 -0
  126. package/package.json +32 -0
  127. package/src/engine.ts +129 -0
  128. package/src/index.ts +28 -0
  129. package/src/scanners/ai-tool-integrity/index.ts +1 -0
  130. package/src/scanners/ai-tool-integrity/patterns/injection-signatures.ts +53 -0
  131. package/src/scanners/ai-tool-integrity/rules/prompt-injection.rule.ts +50 -0
  132. package/src/scanners/ai-tool-integrity/rules/tool-manifest-drift.rule.ts +81 -0
  133. package/src/scanners/ai-tool-integrity/rules/unauthorized-model-call.rule.ts +59 -0
  134. package/src/scanners/ai-tool-integrity/scanner.ts +25 -0
  135. package/src/scanners/dependency-integrity/index.ts +1 -0
  136. package/src/scanners/dependency-integrity/patterns/known-typosquats.ts +41 -0
  137. package/src/scanners/dependency-integrity/rules/hash-validation.rule.ts +72 -0
  138. package/src/scanners/dependency-integrity/rules/transitive-drift.rule.ts +71 -0
  139. package/src/scanners/dependency-integrity/rules/typosquat-check.rule.ts +100 -0
  140. package/src/scanners/dependency-integrity/scanner.ts +25 -0
  141. package/src/scanners/runtime-monitor/index.ts +1 -0
  142. package/src/scanners/runtime-monitor/patterns/known-bad-destinations.ts +55 -0
  143. package/src/scanners/runtime-monitor/rules/filesystem-access.rule.ts +74 -0
  144. package/src/scanners/runtime-monitor/rules/outbound-network.rule.ts +67 -0
  145. package/src/scanners/runtime-monitor/rules/process-spawn.rule.ts +58 -0
  146. package/src/scanners/runtime-monitor/rules/resource-anomaly.rule.ts +79 -0
  147. package/src/scanners/runtime-monitor/scanner.ts +27 -0
  148. package/src/scanners/secrets-exposure/index.ts +1 -0
  149. package/src/scanners/secrets-exposure/patterns/secret-signatures.ts +78 -0
  150. package/src/scanners/secrets-exposure/rules/entropy-check.rule.ts +79 -0
  151. package/src/scanners/secrets-exposure/rules/known-patterns.rule.ts +64 -0
  152. package/src/scanners/secrets-exposure/rules/response-echo.rule.ts +70 -0
  153. package/src/scanners/secrets-exposure/scanner.ts +25 -0
  154. package/src/types/config.types.ts +40 -0
  155. package/src/types/context.types.ts +25 -0
  156. package/src/types/finding.types.ts +36 -0
  157. package/src/types/index.ts +21 -0
  158. package/tsconfig.json +21 -0
  159. package/watchman.config.ts +16 -0
@@ -0,0 +1,78 @@
1
+ export interface SecretSignature {
2
+ pattern: RegExp;
3
+ label: string;
4
+ description: string;
5
+ }
6
+
7
+ export const SECRET_SIGNATURES: SecretSignature[] = [
8
+ {
9
+ pattern: /sk-ant-api\d{2}-[A-Za-z0-9_-]{20,}/,
10
+ label: 'anthropic-api-key',
11
+ description: 'Anthropic API key',
12
+ },
13
+ {
14
+ pattern: /sk-[A-Za-z0-9]{48,}/,
15
+ label: 'openai-api-key',
16
+ description: 'OpenAI API key',
17
+ },
18
+ {
19
+ pattern: /ghp_[A-Za-z0-9]{36,}/,
20
+ label: 'github-pat',
21
+ description: 'GitHub personal access token',
22
+ },
23
+ {
24
+ pattern: /gho_[A-Za-z0-9]{36,}/,
25
+ label: 'github-oauth',
26
+ description: 'GitHub OAuth access token',
27
+ },
28
+ {
29
+ pattern: /github_pat_[A-Za-z0-9_]{20,}/,
30
+ label: 'github-fine-grained-pat',
31
+ description: 'GitHub fine-grained personal access token',
32
+ },
33
+ {
34
+ pattern: /xoxb-[0-9]{10,}-[0-9]{10,}-[A-Za-z0-9]{24,}/,
35
+ label: 'slack-bot-token',
36
+ description: 'Slack bot token',
37
+ },
38
+ {
39
+ pattern: /xoxp-[0-9]{10,}-[0-9]{10,}-[A-Za-z0-9]{24,}/,
40
+ label: 'slack-user-token',
41
+ description: 'Slack user token',
42
+ },
43
+ {
44
+ pattern: /eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}/,
45
+ label: 'supabase-jwt',
46
+ description: 'Supabase JWT (anon or service_role key)',
47
+ },
48
+ {
49
+ pattern: /AKIA[0-9A-Z]{16}/,
50
+ label: 'aws-access-key',
51
+ description: 'AWS access key ID',
52
+ },
53
+ {
54
+ pattern: /npm_[A-Za-z0-9]{36,}/,
55
+ label: 'npm-token',
56
+ description: 'npm access token',
57
+ },
58
+ {
59
+ pattern: /sk_live_[A-Za-z0-9]{24,}/,
60
+ label: 'stripe-secret-key',
61
+ description: 'Stripe secret key',
62
+ },
63
+ {
64
+ pattern: /sq0csp-[A-Za-z0-9_-]{43}/,
65
+ label: 'square-oauth-secret',
66
+ description: 'Square OAuth secret',
67
+ },
68
+ {
69
+ pattern: /SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/,
70
+ label: 'sendgrid-api-key',
71
+ description: 'SendGrid API key',
72
+ },
73
+ {
74
+ pattern: /-----BEGIN (RSA |EC |DSA )?PRIVATE KEY-----/,
75
+ label: 'private-key',
76
+ description: 'Private key file',
77
+ },
78
+ ];
@@ -0,0 +1,79 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { basename } from 'node:path';
3
+ import type { Rule, ScanContext } from '../../../types/index.js';
4
+
5
+ const CODE_EXTENSIONS = new Set([
6
+ '.ts', '.tsx', '.js', '.jsx', '.json', '.yaml', '.yml',
7
+ '.toml', '.cfg', '.ini', '.env',
8
+ ]);
9
+
10
+ function isTargetFile(file: string): boolean {
11
+ const dot = file.lastIndexOf('.');
12
+ if (dot === -1) return basename(file).startsWith('.env');
13
+ return CODE_EXTENSIONS.has(file.slice(dot).toLowerCase());
14
+ }
15
+
16
+ function shannonEntropy(s: string): number {
17
+ if (s.length === 0) return 0;
18
+ const freq = new Map<string, number>();
19
+ for (const ch of s) {
20
+ freq.set(ch, (freq.get(ch) ?? 0) + 1);
21
+ }
22
+ let entropy = 0;
23
+ for (const count of freq.values()) {
24
+ const p = count / s.length;
25
+ entropy -= p * Math.log2(p);
26
+ }
27
+ return entropy;
28
+ }
29
+
30
+ // Match assignment-like patterns: KEY=value, "key": "value", key: value
31
+ const ASSIGNMENT_PATTERN = /(?:['"]?(?:key|token|secret|password|api_key|apikey|auth|credential|private)['"_\s]*[:=]\s*['"]?)([A-Za-z0-9+/=_-]{16,})/gi;
32
+
33
+ const ENTROPY_THRESHOLD = 4.5;
34
+ const MIN_LENGTH = 16;
35
+
36
+ export const entropyCheckRule: Rule = {
37
+ id: 'entropy-check',
38
+ name: 'High Entropy String Detection',
39
+ description: 'Flags high-entropy strings in assignment contexts that may be hardcoded secrets',
40
+
41
+ async run(ctx: ScanContext): Promise<void> {
42
+ for (const file of ctx.files) {
43
+ if (!isTargetFile(file)) continue;
44
+
45
+ let content: string;
46
+ try {
47
+ content = await readFile(file, 'utf-8');
48
+ } catch {
49
+ continue;
50
+ }
51
+
52
+ const lines = content.split('\n');
53
+ for (let i = 0; i < lines.length; i++) {
54
+ const line = lines[i];
55
+ if (/^\s*#/.test(line) || /^\s*\/\//.test(line)) continue;
56
+
57
+ ASSIGNMENT_PATTERN.lastIndex = 0;
58
+ let match: RegExpExecArray | null;
59
+ while ((match = ASSIGNMENT_PATTERN.exec(line)) !== null) {
60
+ const value = match[1];
61
+ if (value.length < MIN_LENGTH) continue;
62
+
63
+ const entropy = shannonEntropy(value);
64
+ if (entropy >= ENTROPY_THRESHOLD) {
65
+ ctx.addFinding({
66
+ rule: 'entropy-check',
67
+ severity: 'high',
68
+ message: `High-entropy string (${entropy.toFixed(2)} bits) in secret-like assignment`,
69
+ location: { file, line: i + 1 },
70
+ evidence: value.slice(0, 8) + '...[REDACTED]',
71
+ suggestion: 'This looks like a hardcoded credential. Move it to an environment variable.',
72
+ metadata: { entropy },
73
+ });
74
+ }
75
+ }
76
+ }
77
+ }
78
+ },
79
+ };
@@ -0,0 +1,64 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { basename } from 'node:path';
3
+ import type { Rule, ScanContext } from '../../../types/index.js';
4
+ import { SECRET_SIGNATURES } from '../patterns/secret-signatures.js';
5
+
6
+ const SKIP_FILES = new Set([
7
+ 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml',
8
+ '.gitignore', '.dockerignore',
9
+ ]);
10
+
11
+ const BINARY_EXTENSIONS = new Set([
12
+ '.png', '.jpg', '.jpeg', '.gif', '.ico', '.woff', '.woff2',
13
+ '.ttf', '.eot', '.zip', '.tar', '.gz', '.pdf',
14
+ ]);
15
+
16
+ function shouldSkip(file: string): boolean {
17
+ const name = basename(file);
18
+ if (SKIP_FILES.has(name)) return true;
19
+ const dot = file.lastIndexOf('.');
20
+ if (dot !== -1 && BINARY_EXTENSIONS.has(file.slice(dot).toLowerCase())) return true;
21
+ return false;
22
+ }
23
+
24
+ export const knownPatternsRule: Rule = {
25
+ id: 'known-patterns',
26
+ name: 'Known Secret Patterns',
27
+ description: 'Matches known API key and credential patterns in source files',
28
+
29
+ async run(ctx: ScanContext): Promise<void> {
30
+ for (const file of ctx.files) {
31
+ if (shouldSkip(file)) continue;
32
+
33
+ let content: string;
34
+ try {
35
+ content = await readFile(file, 'utf-8');
36
+ } catch {
37
+ continue;
38
+ }
39
+
40
+ const lines = content.split('\n');
41
+ for (let i = 0; i < lines.length; i++) {
42
+ const line = lines[i];
43
+
44
+ // Skip lines that are env template placeholders
45
+ if (/^\s*#/.test(line) || /=\s*$/.test(line) || /YOUR_.*_HERE/.test(line)) {
46
+ continue;
47
+ }
48
+
49
+ for (const sig of SECRET_SIGNATURES) {
50
+ if (sig.pattern.test(line)) {
51
+ ctx.addFinding({
52
+ rule: 'known-patterns',
53
+ severity: 'critical',
54
+ message: `${sig.description} found in source code`,
55
+ location: { file, line: i + 1 },
56
+ evidence: line.trim().slice(0, 40) + '...[REDACTED]',
57
+ suggestion: `Move this ${sig.label} to an environment variable. Never commit secrets to source control.`,
58
+ });
59
+ }
60
+ }
61
+ }
62
+ }
63
+ },
64
+ };
@@ -0,0 +1,70 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import type { Rule, ScanContext } from '../../../types/index.js';
3
+
4
+ const CODE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
5
+
6
+ function isCodeFile(file: string): boolean {
7
+ const dot = file.lastIndexOf('.');
8
+ return dot !== -1 && CODE_EXTENSIONS.has(file.slice(dot).toLowerCase());
9
+ }
10
+
11
+ // Patterns where env vars are passed into responses, logs, or error messages
12
+ const ECHO_PATTERNS = [
13
+ {
14
+ pattern: /(?:res\.(?:json|send|write)|console\.(?:log|error|warn))\s*\([^)]*process\.env\.[A-Z_]*(?:KEY|SECRET|TOKEN|PASSWORD)/g,
15
+ label: 'env-var-in-response',
16
+ description: 'Environment variable containing a secret echoed in response or log',
17
+ },
18
+ {
19
+ pattern: /(?:JSON\.stringify|toString)\s*\([^)]*process\.env\b/g,
20
+ label: 'env-serialization',
21
+ description: 'process.env serialized — may leak all environment variables',
22
+ },
23
+ {
24
+ pattern: /(?:res\.(?:json|send))\s*\(\s*\{\s*\.\.\.(?:process\.env|req\.)/g,
25
+ label: 'spread-leak',
26
+ description: 'Object spread may leak sensitive fields into response',
27
+ },
28
+ {
29
+ pattern: /error\.stack|err\.stack/g,
30
+ label: 'stack-trace-exposure',
31
+ description: 'Stack trace exposed — may reveal file paths and internal structure',
32
+ },
33
+ ];
34
+
35
+ export const responseEchoRule: Rule = {
36
+ id: 'response-echo',
37
+ name: 'Response Echo Detection',
38
+ description: 'Detects secrets or sensitive data being echoed in HTTP responses or logs',
39
+
40
+ async run(ctx: ScanContext): Promise<void> {
41
+ const codeFiles = ctx.files.filter(isCodeFile);
42
+
43
+ for (const file of codeFiles) {
44
+ let content: string;
45
+ try {
46
+ content = await readFile(file, 'utf-8');
47
+ } catch {
48
+ continue;
49
+ }
50
+
51
+ const lines = content.split('\n');
52
+ for (let i = 0; i < lines.length; i++) {
53
+ const line = lines[i];
54
+ for (const { pattern, label, description } of ECHO_PATTERNS) {
55
+ pattern.lastIndex = 0;
56
+ if (pattern.test(line)) {
57
+ ctx.addFinding({
58
+ rule: 'response-echo',
59
+ severity: label === 'stack-trace-exposure' ? 'medium' : 'high',
60
+ message: description,
61
+ location: { file, line: i + 1 },
62
+ evidence: line.trim().slice(0, 200),
63
+ suggestion: 'Never echo secrets or full env objects in responses. Sanitize output.',
64
+ });
65
+ }
66
+ }
67
+ }
68
+ }
69
+ },
70
+ };
@@ -0,0 +1,25 @@
1
+ import type { Scanner, ScanContext, Rule } from '../../types/index.js';
2
+ import { entropyCheckRule } from './rules/entropy-check.rule.js';
3
+ import { knownPatternsRule } from './rules/known-patterns.rule.js';
4
+ import { responseEchoRule } from './rules/response-echo.rule.js';
5
+
6
+ const rules: Rule[] = [
7
+ knownPatternsRule,
8
+ entropyCheckRule,
9
+ responseEchoRule,
10
+ ];
11
+
12
+ export const secretsExposureScanner: Scanner = {
13
+ id: 'secrets-exposure',
14
+ name: 'Secrets Exposure',
15
+ description: 'Detects hardcoded secrets, high-entropy strings, and response echo vulnerabilities',
16
+ rules,
17
+
18
+ async scan(ctx: ScanContext): Promise<void> {
19
+ for (const rule of rules) {
20
+ const ruleConfig = ctx.scannerConfig.rules?.[rule.id];
21
+ if (ruleConfig?.enabled === false) continue;
22
+ await rule.run(ctx);
23
+ }
24
+ },
25
+ };
@@ -0,0 +1,40 @@
1
+ export type Severity = 'critical' | 'high' | 'medium' | 'low' | 'info';
2
+
3
+ export type ScannerId =
4
+ | 'ai-tool-integrity'
5
+ | 'secrets-exposure'
6
+ | 'dependency-integrity'
7
+ | 'runtime-monitor';
8
+
9
+ export interface RuleConfig {
10
+ enabled: boolean;
11
+ severity?: Severity;
12
+ options?: Record<string, unknown>;
13
+ }
14
+
15
+ export interface ScannerConfig {
16
+ enabled: boolean;
17
+ rules?: Record<string, RuleConfig>;
18
+ }
19
+
20
+ export interface WatchmanConfig {
21
+ projectRoot: string;
22
+ scanners: Partial<Record<ScannerId, ScannerConfig>>;
23
+ include?: string[];
24
+ exclude?: string[];
25
+ failOn?: Severity;
26
+ quiet?: boolean;
27
+ maxFindings?: number;
28
+ }
29
+
30
+ export const DEFAULT_CONFIG: Omit<WatchmanConfig, 'projectRoot'> = {
31
+ scanners: {
32
+ 'ai-tool-integrity': { enabled: true },
33
+ 'secrets-exposure': { enabled: true },
34
+ 'dependency-integrity': { enabled: true },
35
+ 'runtime-monitor': { enabled: false },
36
+ },
37
+ exclude: ['node_modules', '.git', 'dist', 'build', '.next'],
38
+ failOn: 'high',
39
+ quiet: false,
40
+ };
@@ -0,0 +1,25 @@
1
+ import type { WatchmanConfig, ScannerConfig, ScannerId } from './config.types.js';
2
+ import type { Finding } from './finding.types.js';
3
+
4
+ export interface ScanContext {
5
+ config: WatchmanConfig;
6
+ scannerId: ScannerId;
7
+ scannerConfig: ScannerConfig;
8
+ files: string[];
9
+ addFinding: (finding: Omit<Finding, 'id' | 'scanner'>) => void;
10
+ }
11
+
12
+ export interface Rule {
13
+ id: string;
14
+ name: string;
15
+ description: string;
16
+ run: (ctx: ScanContext) => Promise<void>;
17
+ }
18
+
19
+ export interface Scanner {
20
+ id: ScannerId;
21
+ name: string;
22
+ description: string;
23
+ rules: Rule[];
24
+ scan: (ctx: ScanContext) => Promise<void>;
25
+ }
@@ -0,0 +1,36 @@
1
+ import type { Severity, ScannerId } from './config.types.js';
2
+
3
+ export interface SourceLocation {
4
+ file: string;
5
+ line?: number;
6
+ column?: number;
7
+ }
8
+
9
+ export interface Finding {
10
+ id: string;
11
+ scanner: ScannerId;
12
+ rule: string;
13
+ severity: Severity;
14
+ message: string;
15
+ location?: SourceLocation;
16
+ evidence?: string;
17
+ suggestion?: string;
18
+ metadata?: Record<string, unknown>;
19
+ }
20
+
21
+ export interface ScanResult {
22
+ scanner: ScannerId;
23
+ findings: Finding[];
24
+ duration: number;
25
+ filesScanned: number;
26
+ }
27
+
28
+ export interface WatchmanReport {
29
+ timestamp: string;
30
+ projectRoot: string;
31
+ results: ScanResult[];
32
+ totalFindings: number;
33
+ maxSeverity: Severity | null;
34
+ passed: boolean;
35
+ duration: number;
36
+ }
@@ -0,0 +1,21 @@
1
+ export type {
2
+ Severity,
3
+ ScannerId,
4
+ RuleConfig,
5
+ ScannerConfig,
6
+ WatchmanConfig,
7
+ } from './config.types.js';
8
+ export { DEFAULT_CONFIG } from './config.types.js';
9
+
10
+ export type {
11
+ SourceLocation,
12
+ Finding,
13
+ ScanResult,
14
+ WatchmanReport,
15
+ } from './finding.types.js';
16
+
17
+ export type {
18
+ ScanContext,
19
+ Rule,
20
+ Scanner,
21
+ } from './context.types.js';
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022"],
7
+ "outDir": "dist",
8
+ "rootDir": "src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true,
16
+ "resolveJsonModule": true,
17
+ "isolatedModules": true
18
+ },
19
+ "include": ["src"],
20
+ "exclude": ["node_modules", "dist", "**/*.test.ts"]
21
+ }
@@ -0,0 +1,16 @@
1
+ import type { WatchmanConfig } from './src/types/config.types.js';
2
+
3
+ const config: WatchmanConfig = {
4
+ projectRoot: process.cwd(),
5
+ scanners: {
6
+ 'ai-tool-integrity': { enabled: true },
7
+ 'secrets-exposure': { enabled: true },
8
+ 'dependency-integrity': { enabled: true },
9
+ 'runtime-monitor': { enabled: false },
10
+ },
11
+ exclude: ['node_modules', '.git', 'dist', 'build', '.next'],
12
+ failOn: 'high',
13
+ quiet: false,
14
+ };
15
+
16
+ export default config;