@euconform/core 1.0.1 → 1.3.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 (183) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +12 -0
  3. package/dist/evidence/bundle.d.ts +23 -0
  4. package/dist/evidence/bundle.d.ts.map +1 -0
  5. package/dist/evidence/bundle.js +44 -0
  6. package/dist/evidence/compliance-evaluation.d.ts +12 -0
  7. package/dist/evidence/compliance-evaluation.d.ts.map +1 -0
  8. package/dist/evidence/compliance-evaluation.js +78 -0
  9. package/dist/evidence/hash.d.ts +2 -0
  10. package/dist/evidence/hash.d.ts.map +1 -0
  11. package/dist/evidence/hash.js +4 -0
  12. package/dist/evidence/index.d.ts +13 -0
  13. package/dist/evidence/index.d.ts.map +1 -0
  14. package/dist/evidence/index.js +11 -0
  15. package/dist/evidence/markdown.d.ts +9 -0
  16. package/dist/evidence/markdown.d.ts.map +1 -0
  17. package/dist/evidence/markdown.js +169 -0
  18. package/dist/evidence/output.d.ts +13 -0
  19. package/dist/evidence/output.d.ts.map +1 -0
  20. package/dist/evidence/output.js +547 -0
  21. package/dist/evidence/types.d.ts +230 -0
  22. package/dist/evidence/types.d.ts.map +1 -0
  23. package/dist/evidence/types.js +7 -0
  24. package/dist/evidence/validate.d.ts +8 -0
  25. package/dist/evidence/validate.d.ts.map +1 -0
  26. package/dist/evidence/validate.js +175 -0
  27. package/dist/index.d.ts +2 -2
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +1 -1
  30. package/dist/inference/ollama-client.d.ts +3 -0
  31. package/dist/inference/ollama-client.d.ts.map +1 -1
  32. package/dist/inference/ollama-client.js +69 -71
  33. package/dist/legal-checks/annex-iv-report.d.ts +3 -0
  34. package/dist/legal-checks/annex-iv-report.d.ts.map +1 -1
  35. package/dist/legal-checks/annex-iv-report.js +1 -0
  36. package/dist/legal-checks/deadline-timeline.d.ts +57 -0
  37. package/dist/legal-checks/deadline-timeline.d.ts.map +1 -0
  38. package/dist/legal-checks/deadline-timeline.js +265 -0
  39. package/dist/legal-checks/gap-analysis.d.ts +48 -0
  40. package/dist/legal-checks/gap-analysis.d.ts.map +1 -0
  41. package/dist/legal-checks/gap-analysis.js +397 -0
  42. package/dist/legal-checks/gpai-classifier.d.ts +49 -0
  43. package/dist/legal-checks/gpai-classifier.d.ts.map +1 -0
  44. package/dist/legal-checks/gpai-classifier.js +197 -0
  45. package/dist/legal-checks/index.d.ts +6 -0
  46. package/dist/legal-checks/index.d.ts.map +1 -1
  47. package/dist/legal-checks/index.js +3 -0
  48. package/dist/scanner/aggregator.d.ts +7 -0
  49. package/dist/scanner/aggregator.d.ts.map +1 -0
  50. package/dist/scanner/aggregator.js +200 -0
  51. package/dist/scanner/detectors/ai-framework-detector.d.ts +3 -0
  52. package/dist/scanner/detectors/ai-framework-detector.d.ts.map +1 -0
  53. package/dist/scanner/detectors/ai-framework-detector.js +74 -0
  54. package/dist/scanner/detectors/ai-provider-detector.d.ts +3 -0
  55. package/dist/scanner/detectors/ai-provider-detector.d.ts.map +1 -0
  56. package/dist/scanner/detectors/ai-provider-detector.js +100 -0
  57. package/dist/scanner/detectors/compliance-detector.d.ts +3 -0
  58. package/dist/scanner/detectors/compliance-detector.d.ts.map +1 -0
  59. package/dist/scanner/detectors/compliance-detector.js +201 -0
  60. package/dist/scanner/detectors/config-detector.d.ts +3 -0
  61. package/dist/scanner/detectors/config-detector.d.ts.map +1 -0
  62. package/dist/scanner/detectors/config-detector.js +248 -0
  63. package/dist/scanner/detectors/framework-detector.d.ts +3 -0
  64. package/dist/scanner/detectors/framework-detector.d.ts.map +1 -0
  65. package/dist/scanner/detectors/framework-detector.js +141 -0
  66. package/dist/scanner/detectors/index.d.ts +8 -0
  67. package/dist/scanner/detectors/index.d.ts.map +1 -0
  68. package/dist/scanner/detectors/index.js +37 -0
  69. package/dist/scanner/detectors/local-inference-detector.d.ts +3 -0
  70. package/dist/scanner/detectors/local-inference-detector.d.ts.map +1 -0
  71. package/dist/scanner/detectors/local-inference-detector.js +93 -0
  72. package/dist/scanner/detectors/markdown-detector.d.ts +3 -0
  73. package/dist/scanner/detectors/markdown-detector.d.ts.map +1 -0
  74. package/dist/scanner/detectors/markdown-detector.js +139 -0
  75. package/dist/scanner/detectors/package-json-detector.d.ts +3 -0
  76. package/dist/scanner/detectors/package-json-detector.d.ts.map +1 -0
  77. package/dist/scanner/detectors/package-json-detector.js +166 -0
  78. package/dist/scanner/detectors/rag-detector.d.ts +3 -0
  79. package/dist/scanner/detectors/rag-detector.d.ts.map +1 -0
  80. package/dist/scanner/detectors/rag-detector.js +132 -0
  81. package/dist/scanner/detectors/shared.d.ts +18 -0
  82. package/dist/scanner/detectors/shared.d.ts.map +1 -0
  83. package/dist/scanner/detectors/shared.js +36 -0
  84. package/dist/scanner/discovery.d.ts +14 -0
  85. package/dist/scanner/discovery.d.ts.map +1 -0
  86. package/dist/scanner/discovery.js +235 -0
  87. package/dist/scanner/index.d.ts +9 -0
  88. package/dist/scanner/index.d.ts.map +1 -0
  89. package/dist/scanner/index.js +8 -0
  90. package/package.json +6 -3
  91. package/src/evidence/bundle.ts +65 -0
  92. package/src/evidence/compliance-evaluation.ts +114 -0
  93. package/src/evidence/hash.ts +5 -0
  94. package/src/evidence/index.ts +51 -0
  95. package/src/evidence/markdown.ts +226 -0
  96. package/src/evidence/output.ts +717 -0
  97. package/src/evidence/types.ts +329 -0
  98. package/src/evidence/validate.ts +270 -0
  99. package/src/index.ts +25 -0
  100. package/src/inference/ollama-client.ts +93 -86
  101. package/src/legal-checks/annex-iv-report.ts +4 -0
  102. package/src/legal-checks/deadline-timeline.ts +313 -0
  103. package/src/legal-checks/gap-analysis.ts +454 -0
  104. package/src/legal-checks/gpai-classifier.ts +281 -0
  105. package/src/legal-checks/index.ts +33 -0
  106. package/src/scanner/aggregator.ts +258 -0
  107. package/src/scanner/detectors/ai-framework-detector.ts +94 -0
  108. package/src/scanner/detectors/ai-provider-detector.ts +135 -0
  109. package/src/scanner/detectors/compliance-detector.ts +232 -0
  110. package/src/scanner/detectors/config-detector.ts +316 -0
  111. package/src/scanner/detectors/framework-detector.ts +163 -0
  112. package/src/scanner/detectors/index.ts +40 -0
  113. package/src/scanner/detectors/local-inference-detector.ts +124 -0
  114. package/src/scanner/detectors/markdown-detector.ts +176 -0
  115. package/src/scanner/detectors/package-json-detector.ts +202 -0
  116. package/src/scanner/detectors/rag-detector.ts +162 -0
  117. package/src/scanner/detectors/shared.ts +45 -0
  118. package/src/scanner/discovery.ts +290 -0
  119. package/src/scanner/index.ts +9 -0
  120. package/tests/evidence/bundle.test.ts +128 -0
  121. package/tests/evidence/ecef-schemas.test.ts +316 -0
  122. package/tests/evidence/hash.test.ts +25 -0
  123. package/tests/evidence/markdown.test.ts +92 -0
  124. package/tests/evidence/output.test.ts +203 -0
  125. package/tests/fixtures/compliance-good/README.md +14 -0
  126. package/tests/fixtures/compliance-good/lib/ai-service.ts +17 -0
  127. package/tests/fixtures/compliance-good/lib/review-flow.ts +11 -0
  128. package/tests/fixtures/compliance-good/package.json +10 -0
  129. package/tests/fixtures/compliance-good/tests/bias-check.test.ts +13 -0
  130. package/tests/fixtures/nextjs-openai/README.md +5 -0
  131. package/tests/fixtures/nextjs-openai/app/page.tsx +11 -0
  132. package/tests/fixtures/nextjs-openai/package.json +9 -0
  133. package/tests/fixtures/ollama-rag/.env.example +2 -0
  134. package/tests/fixtures/ollama-rag/docker-compose.yml +9 -0
  135. package/tests/fixtures/ollama-rag/package.json +9 -0
  136. package/tests/fixtures/ollama-rag/server.ts +21 -0
  137. package/tests/fixtures/plain-webapp/README.md +3 -0
  138. package/tests/fixtures/plain-webapp/index.js +5 -0
  139. package/tests/fixtures/plain-webapp/package.json +8 -0
  140. package/tests/gpai-classifier.test.ts +125 -0
  141. package/tests/gpai-timeline.test.ts +55 -0
  142. package/tests/scanner/aggregator.test.ts +122 -0
  143. package/tests/scanner/detectors.test.ts +274 -0
  144. package/tests/scanner/discovery.test.ts +87 -0
  145. package/vitest.config.ts +1 -0
  146. package/.turbo/turbo-check-types.log +0 -4
  147. package/.turbo/turbo-test.log +0 -364
  148. package/coverage/base.css +0 -224
  149. package/coverage/block-navigation.js +0 -87
  150. package/coverage/clover.xml +0 -3282
  151. package/coverage/coverage-final.json +0 -20
  152. package/coverage/favicon.png +0 -0
  153. package/coverage/index.html +0 -161
  154. package/coverage/prettify.css +0 -1
  155. package/coverage/prettify.js +0 -2
  156. package/coverage/sort-arrow-sprite.png +0 -0
  157. package/coverage/sorter.js +0 -210
  158. package/coverage/src/fairness-metrics.ts.html +0 -1579
  159. package/coverage/src/index.html +0 -161
  160. package/coverage/src/index.ts.html +0 -535
  161. package/coverage/src/inference/browser-inference.ts.html +0 -859
  162. package/coverage/src/inference/capability-detection.ts.html +0 -2227
  163. package/coverage/src/inference/index.html +0 -176
  164. package/coverage/src/inference/index.ts.html +0 -553
  165. package/coverage/src/inference/inference-factory.ts.html +0 -751
  166. package/coverage/src/inference/ollama-client.ts.html +0 -1627
  167. package/coverage/src/legal-checks/annex-iv-report.ts.html +0 -556
  168. package/coverage/src/legal-checks/bias-metrics.ts.html +0 -1855
  169. package/coverage/src/legal-checks/data-governance.ts.html +0 -529
  170. package/coverage/src/legal-checks/human-oversight-logging.ts.html +0 -490
  171. package/coverage/src/legal-checks/index.html +0 -206
  172. package/coverage/src/legal-checks/index.ts.html +0 -250
  173. package/coverage/src/legal-checks/risk-classifier.ts.html +0 -940
  174. package/coverage/src/legal-checks/robustness-transparency.ts.html +0 -571
  175. package/coverage/src/metrics/crows-pairs-bias.ts.html +0 -820
  176. package/coverage/src/metrics/index.html +0 -146
  177. package/coverage/src/metrics/index.ts.html +0 -151
  178. package/coverage/src/metrics/stereotype-bias.ts.html +0 -886
  179. package/coverage/src/risk-engine.ts.html +0 -1660
  180. package/coverage/src/types.ts.html +0 -547
  181. package/dist/legal-checks/disclaimer.d.ts +0 -2
  182. package/dist/legal-checks/disclaimer.d.ts.map +0 -1
  183. package/dist/legal-checks/disclaimer.js +0 -1
@@ -0,0 +1,135 @@
1
+ import type { DetectedSignal, DetectorContext } from "../../evidence/types";
2
+ import { SOURCE_EXTENSIONS } from "./shared";
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Provider patterns
6
+ // ---------------------------------------------------------------------------
7
+
8
+ interface ProviderPattern {
9
+ id: string;
10
+ name: string;
11
+ /** Regex applied per line to detect imports/requires */
12
+ importPattern: RegExp;
13
+ }
14
+
15
+ const PROVIDER_IMPORTS: ProviderPattern[] = [
16
+ {
17
+ id: "ai-provider-openai",
18
+ name: "OpenAI SDK",
19
+ importPattern: /(?:from\s+['"]openai['"]|require\s*\(\s*['"]openai['"])/,
20
+ },
21
+ {
22
+ id: "ai-provider-anthropic",
23
+ name: "Anthropic SDK",
24
+ importPattern:
25
+ /(?:from\s+['"]@anthropic-ai\/sdk['"]|require\s*\(\s*['"]@anthropic-ai\/sdk['"])/,
26
+ },
27
+ {
28
+ id: "ai-provider-cohere",
29
+ name: "Cohere SDK",
30
+ importPattern: /(?:from\s+['"]cohere-ai['"]|require\s*\(\s*['"]cohere-ai['"])/,
31
+ },
32
+ {
33
+ id: "ai-provider-google-genai",
34
+ name: "Google Generative AI",
35
+ importPattern:
36
+ /(?:from\s+['"]@google\/generative-ai['"]|require\s*\(\s*['"]@google\/generative-ai['"])/,
37
+ },
38
+ {
39
+ id: "ai-provider-mistral",
40
+ name: "Mistral AI SDK",
41
+ importPattern:
42
+ /(?:from\s+['"]@mistralai\/mistralai['"]|require\s*\(\s*['"]@mistralai\/mistralai['"])/,
43
+ },
44
+ ];
45
+
46
+ interface ApiEndpoint {
47
+ id: string;
48
+ name: string;
49
+ pattern: RegExp;
50
+ }
51
+
52
+ const API_ENDPOINTS: ApiEndpoint[] = [
53
+ {
54
+ id: "ai-provider-openai-api",
55
+ name: "OpenAI API endpoint",
56
+ pattern: /api\.openai\.com/,
57
+ },
58
+ {
59
+ id: "ai-provider-anthropic-api",
60
+ name: "Anthropic API endpoint",
61
+ pattern: /api\.anthropic\.com/,
62
+ },
63
+ {
64
+ id: "ai-provider-cohere-api",
65
+ name: "Cohere API endpoint",
66
+ pattern: /api\.cohere\.ai/,
67
+ },
68
+ ];
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Detector
72
+ // ---------------------------------------------------------------------------
73
+
74
+ export function detectAiProviderSignals(ctx: DetectorContext): DetectedSignal[] {
75
+ const { file } = ctx;
76
+ if (!SOURCE_EXTENSIONS.has(file.extension)) return [];
77
+
78
+ const lines = ctx.lines;
79
+ const signalMap = new Map<string, DetectedSignal>();
80
+
81
+ for (let i = 0; i < lines.length; i++) {
82
+ const line = lines[i];
83
+ if (!line) continue;
84
+ const lineNum = i + 1;
85
+
86
+ // Check import/require patterns
87
+ for (const provider of PROVIDER_IMPORTS) {
88
+ if (provider.importPattern.test(line)) {
89
+ addEvidence(signalMap, provider.id, provider.name, "medium", {
90
+ file: file.relativePath,
91
+ line: lineNum,
92
+ snippet: line.trim().slice(0, 200),
93
+ });
94
+ }
95
+ }
96
+
97
+ // Check API endpoint URLs
98
+ for (const endpoint of API_ENDPOINTS) {
99
+ if (endpoint.pattern.test(line)) {
100
+ addEvidence(signalMap, endpoint.id, endpoint.name, "medium", {
101
+ file: file.relativePath,
102
+ line: lineNum,
103
+ snippet: line.trim().slice(0, 200),
104
+ });
105
+ }
106
+ }
107
+ }
108
+
109
+ return Array.from(signalMap.values());
110
+ }
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // Helpers
114
+ // ---------------------------------------------------------------------------
115
+
116
+ function addEvidence(
117
+ map: Map<string, DetectedSignal>,
118
+ id: string,
119
+ name: string,
120
+ confidence: "high" | "medium" | "low",
121
+ evidence: { file: string; line?: number; snippet: string }
122
+ ): void {
123
+ const existing = map.get(id);
124
+ if (existing) {
125
+ existing.evidence.push(evidence);
126
+ } else {
127
+ map.set(id, {
128
+ id,
129
+ name,
130
+ category: "ai-provider",
131
+ confidence,
132
+ evidence: [evidence],
133
+ });
134
+ }
135
+ }
@@ -0,0 +1,232 @@
1
+ import { IMPLEMENTATION_PROVENANCE, isUserFacingPath } from "../../evidence/compliance-evaluation";
2
+ import type {
3
+ ConfidenceLevel,
4
+ DetectedSignal,
5
+ DetectorContext,
6
+ SignalCategory,
7
+ SignalEvidence,
8
+ } from "../../evidence/types";
9
+ import { SOURCE_EXTENSIONS } from "./shared";
10
+
11
+ interface ComplianceRule {
12
+ id: string;
13
+ name: string;
14
+ category: SignalCategory;
15
+ pattern: RegExp;
16
+ confidence: ConfidenceLevel;
17
+ requiresUserFacingPath?: boolean;
18
+ }
19
+
20
+ const rules: ComplianceRule[] = [
21
+ // --- compliance-disclosure ---
22
+ {
23
+ id: "compliance-disclosure-ai-generated",
24
+ name: "AI disclosure text",
25
+ category: "compliance-disclosure",
26
+ pattern: /\b(?:generated\s+by\s+ai|ai[- ]generated|powered\s+by\s+ai)\b/i,
27
+ confidence: "medium",
28
+ },
29
+ {
30
+ id: "compliance-disclosure-ai-notice",
31
+ name: "AI transparency notice",
32
+ category: "compliance-disclosure",
33
+ pattern: /\b(?:artificial\s+intelligence|machine\s+learning\s+model|transparency\s+notice)\b/i,
34
+ confidence: "medium",
35
+ },
36
+
37
+ // --- compliance-logging ---
38
+ {
39
+ id: "compliance-logging-structured",
40
+ name: "Structured logging library",
41
+ category: "compliance-logging",
42
+ pattern:
43
+ /\bfrom\s+['"](?:winston|pino|bunyan)['"]|\brequire\(\s*['"](?:winston|pino|bunyan)['"]\)/,
44
+ confidence: "medium",
45
+ },
46
+ {
47
+ id: "compliance-logging-audit",
48
+ name: "Audit trail pattern",
49
+ category: "compliance-logging",
50
+ pattern: /\b(?:audit[._-]?log|audit[._-]?trail|log[._-]?audit)\b/i,
51
+ confidence: "medium",
52
+ },
53
+ {
54
+ id: "compliance-logging-middleware",
55
+ name: "Logging middleware",
56
+ category: "compliance-logging",
57
+ pattern: /\b(?:logging[Mm]iddleware|requestLogger|morgan)\b/,
58
+ confidence: "low",
59
+ },
60
+
61
+ // --- compliance-oversight ---
62
+ {
63
+ id: "compliance-oversight-review",
64
+ name: "Human review flow",
65
+ category: "compliance-oversight",
66
+ pattern: /\b(?:human[_-]?review|manual[_-]?review|approval[_-]?flow|review[_-]?gate)\b/i,
67
+ confidence: "medium",
68
+ },
69
+ {
70
+ id: "compliance-oversight-approve-reject",
71
+ name: "Approval/rejection mechanism",
72
+ category: "compliance-oversight",
73
+ pattern: /\b(?:onApprove|onReject|handleApproval|approveAction|rejectAction)\b/,
74
+ confidence: "medium",
75
+ },
76
+ {
77
+ id: "compliance-oversight-override",
78
+ name: "Override mechanism",
79
+ category: "compliance-oversight",
80
+ pattern: /\b(?:override[_-]?ai|human[_-]?override|manual[_-]?override)\b/i,
81
+ confidence: "medium",
82
+ },
83
+
84
+ // --- compliance-bias ---
85
+ {
86
+ id: "compliance-bias-fairness",
87
+ name: "Fairness/bias testing",
88
+ category: "compliance-bias",
89
+ pattern: /\b(?:fairness[_-]?test|bias[_-]?test|disparate[_-]?impact|demographic[_-]?parity)\b/i,
90
+ confidence: "medium",
91
+ },
92
+ {
93
+ id: "compliance-bias-benchmark",
94
+ name: "Bias benchmark reference",
95
+ category: "compliance-bias",
96
+ pattern: /\bCrowS[_-]?Pairs\b/i,
97
+ confidence: "medium",
98
+ },
99
+ {
100
+ id: "compliance-bias-euconform",
101
+ name: "EuConform bias utilities",
102
+ category: "compliance-bias",
103
+ pattern: /\bfrom\s+['"]@euconform\/core.*bias/,
104
+ confidence: "medium",
105
+ },
106
+
107
+ // --- compliance-data ---
108
+ {
109
+ id: "compliance-data-quality",
110
+ name: "Data quality checks",
111
+ category: "compliance-data",
112
+ pattern: /\b(?:data[_-]?quality|data[_-]?validation|dataset[_-]?doc|data[_-]?lineage)\b/i,
113
+ confidence: "medium",
114
+ },
115
+ {
116
+ id: "compliance-data-pii",
117
+ name: "PII handling / anonymization",
118
+ category: "compliance-data",
119
+ pattern: /\b(?:pii[_-]?filter|anonymi[sz]e|pseudonymi[sz]e|redact[_-]?pii)\b/i,
120
+ confidence: "medium",
121
+ },
122
+ {
123
+ id: "compliance-data-gdpr",
124
+ name: "GDPR reference",
125
+ category: "compliance-data",
126
+ pattern: /\bGDPR\b|data[_-]?protection[_-]?regulation/i,
127
+ confidence: "low",
128
+ },
129
+
130
+ // --- compliance-reporting ---
131
+ {
132
+ id: "compliance-reporting-json",
133
+ name: "JSON export capability",
134
+ category: "compliance-reporting",
135
+ pattern:
136
+ /\b(?:exportJSON|downloadJSON|writeJSONFile|saveJSONReport|exportAuditJSON|exportAnnexJSON|generateAnnexJson)\b|(?:writeFile(?:Sync)?|saveAs|download)\s*\([^)]*(?:report|audit|annex|compliance)[^)]*\.json/i,
137
+ confidence: "medium",
138
+ },
139
+ {
140
+ id: "compliance-reporting-pdf",
141
+ name: "PDF export capability",
142
+ category: "compliance-reporting",
143
+ pattern:
144
+ /\b(?:jspdf|pdf[_-]?lib|pdfmake|generatePDF|exportPDF|downloadPDF|renderPDF|generateAnnexPdf)\b|puppeteer.*\.pdf/i,
145
+ confidence: "medium",
146
+ },
147
+ {
148
+ id: "compliance-reporting-report",
149
+ name: "Report generation",
150
+ category: "compliance-reporting",
151
+ pattern:
152
+ /\b(?:generateReport|buildReport|exportReport|downloadReport|createReport|annex[_-]?iv[_-]?report|compliance[_-]?report|audit[_-]?report)\b/i,
153
+ confidence: "medium",
154
+ },
155
+
156
+ // --- compliance-incident ---
157
+ {
158
+ id: "compliance-incident-sentry",
159
+ name: "Sentry error reporting",
160
+ category: "compliance-incident",
161
+ pattern: /\bfrom\s+['"]@sentry\/|\bSentry\.init\b|\brequire\(\s*['"]@sentry\//,
162
+ confidence: "medium",
163
+ },
164
+ {
165
+ id: "compliance-incident-boundary",
166
+ name: "Error boundary",
167
+ category: "compliance-incident",
168
+ pattern: /\bErrorBoundary\b|componentDidCatch/,
169
+ confidence: "medium",
170
+ },
171
+ {
172
+ id: "compliance-incident-response",
173
+ name: "Incident response hook",
174
+ category: "compliance-incident",
175
+ pattern: /\b(?:incident[_-]?report|incident[_-]?response|useIncident|alerting[_-]?hook)\b/i,
176
+ confidence: "medium",
177
+ },
178
+ ];
179
+
180
+ function canEvaluateRule(rule: ComplianceRule, file: DetectorContext["file"]): boolean {
181
+ if (!IMPLEMENTATION_PROVENANCE.has(file.provenance)) return false;
182
+ if (rule.category === "compliance-disclosure") return isUserFacingPath(file.relativePath);
183
+ if (rule.requiresUserFacingPath) return isUserFacingPath(file.relativePath);
184
+ return true;
185
+ }
186
+
187
+ function gatherEvidence(
188
+ lines: string[],
189
+ pattern: RegExp,
190
+ filePath: string,
191
+ maxItems: number
192
+ ): SignalEvidence[] {
193
+ const evidence: SignalEvidence[] = [];
194
+ for (let i = 0; i < lines.length; i++) {
195
+ const line = lines[i];
196
+ if (!line) continue;
197
+ if (pattern.test(line)) {
198
+ evidence.push({ file: filePath, line: i + 1, snippet: line.trim().slice(0, 120) });
199
+ if (evidence.length >= maxItems) break;
200
+ }
201
+ }
202
+ return evidence;
203
+ }
204
+
205
+ export function detectCompliance(ctx: DetectorContext): DetectedSignal[] {
206
+ const { file } = ctx;
207
+ if (!SOURCE_EXTENSIONS.has(file.extension)) return [];
208
+
209
+ const signalMap = new Map<string, DetectedSignal>();
210
+
211
+ for (const rule of rules) {
212
+ if (!canEvaluateRule(rule, file)) continue;
213
+
214
+ const evidence = gatherEvidence(ctx.lines, rule.pattern, file.relativePath, 3);
215
+ if (evidence.length === 0) continue;
216
+
217
+ const existing = signalMap.get(rule.id);
218
+ if (existing) {
219
+ existing.evidence.push(...evidence);
220
+ } else {
221
+ signalMap.set(rule.id, {
222
+ id: rule.id,
223
+ name: rule.name,
224
+ category: rule.category,
225
+ confidence: rule.confidence,
226
+ evidence,
227
+ });
228
+ }
229
+ }
230
+
231
+ return Array.from(signalMap.values());
232
+ }
@@ -0,0 +1,316 @@
1
+ import type {
2
+ ConfidenceLevel,
3
+ DetectedSignal,
4
+ DetectorContext,
5
+ SignalCategory,
6
+ SignalEvidence,
7
+ } from "../../evidence/types";
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // .env file detection — variable names only, never values
11
+ // ---------------------------------------------------------------------------
12
+
13
+ interface EnvKeyRule {
14
+ id: string;
15
+ name: string;
16
+ category: SignalCategory;
17
+ pattern: RegExp;
18
+ confidence: ConfidenceLevel;
19
+ }
20
+
21
+ const envKeyRules: EnvKeyRule[] = [
22
+ {
23
+ id: "ai-provider-openai",
24
+ name: "OpenAI API key",
25
+ category: "ai-provider",
26
+ pattern: /^OPENAI_API_KEY\b/,
27
+ confidence: "high",
28
+ },
29
+ {
30
+ id: "ai-provider-anthropic",
31
+ name: "Anthropic API key",
32
+ category: "ai-provider",
33
+ pattern: /^ANTHROPIC_API_KEY\b/,
34
+ confidence: "high",
35
+ },
36
+ {
37
+ id: "ai-provider-azure-openai",
38
+ name: "Azure OpenAI key",
39
+ category: "ai-provider",
40
+ pattern: /^AZURE_OPENAI_/,
41
+ confidence: "high",
42
+ },
43
+ {
44
+ id: "ai-provider-google-ai",
45
+ name: "Google AI key",
46
+ category: "ai-provider",
47
+ pattern: /^(?:GOOGLE_AI_API_KEY|GEMINI_API_KEY)\b/,
48
+ confidence: "high",
49
+ },
50
+ {
51
+ id: "ai-provider-cohere",
52
+ name: "Cohere API key",
53
+ category: "ai-provider",
54
+ pattern: /^COHERE_API_KEY\b/,
55
+ confidence: "high",
56
+ },
57
+ {
58
+ id: "ai-provider-replicate",
59
+ name: "Replicate API token",
60
+ category: "ai-provider",
61
+ pattern: /^REPLICATE_API_TOKEN\b/,
62
+ confidence: "high",
63
+ },
64
+ {
65
+ id: "ai-provider-huggingface",
66
+ name: "HuggingFace token",
67
+ category: "ai-provider",
68
+ pattern: /^HUGGINGFACE_TOKEN\b|^HF_TOKEN\b/,
69
+ confidence: "high",
70
+ },
71
+ {
72
+ id: "local-inference-ollama",
73
+ name: "Ollama host config",
74
+ category: "local-inference",
75
+ pattern: /^OLLAMA_HOST\b|^OLLAMA_BASE_URL\b/,
76
+ confidence: "high",
77
+ },
78
+ {
79
+ id: "rag-pinecone",
80
+ name: "Pinecone API key",
81
+ category: "rag",
82
+ pattern: /^PINECONE_API_KEY\b/,
83
+ confidence: "high",
84
+ },
85
+ {
86
+ id: "rag-weaviate",
87
+ name: "Weaviate config",
88
+ category: "rag",
89
+ pattern: /^WEAVIATE_/,
90
+ confidence: "high",
91
+ },
92
+ {
93
+ id: "rag-chroma",
94
+ name: "ChromaDB config",
95
+ category: "rag",
96
+ pattern: /^CHROMA_/,
97
+ confidence: "high",
98
+ },
99
+ ];
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // Docker Compose detection
103
+ // ---------------------------------------------------------------------------
104
+
105
+ interface DockerServiceRule {
106
+ id: string;
107
+ name: string;
108
+ category: SignalCategory;
109
+ pattern: RegExp;
110
+ confidence: ConfidenceLevel;
111
+ }
112
+
113
+ const dockerServiceRules: DockerServiceRule[] = [
114
+ {
115
+ id: "local-inference-ollama-docker",
116
+ name: "Ollama Docker service",
117
+ category: "local-inference",
118
+ pattern: /\bollama\/ollama\b|image:\s*ollama/,
119
+ confidence: "high",
120
+ },
121
+ {
122
+ id: "rag-chromadb-docker",
123
+ name: "ChromaDB Docker service",
124
+ category: "rag",
125
+ pattern: /\bchromadb\/chroma\b|image:\s*chroma/,
126
+ confidence: "high",
127
+ },
128
+ {
129
+ id: "rag-weaviate-docker",
130
+ name: "Weaviate Docker service",
131
+ category: "rag",
132
+ pattern: /\bsemitechnologies\/weaviate\b|image:\s*weaviate/,
133
+ confidence: "high",
134
+ },
135
+ {
136
+ id: "rag-redis-vector",
137
+ name: "Redis with vector module",
138
+ category: "rag",
139
+ pattern: /redis\/redis-stack|redisearch|redis-stack-server/,
140
+ confidence: "high",
141
+ },
142
+ {
143
+ id: "rag-qdrant-docker",
144
+ name: "Qdrant Docker service",
145
+ category: "rag",
146
+ pattern: /\bqdrant\/qdrant\b|image:\s*qdrant/,
147
+ confidence: "high",
148
+ },
149
+ ];
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // Dockerfile detection
153
+ // ---------------------------------------------------------------------------
154
+
155
+ interface DockerfileRule {
156
+ id: string;
157
+ name: string;
158
+ category: SignalCategory;
159
+ pattern: RegExp;
160
+ confidence: ConfidenceLevel;
161
+ }
162
+
163
+ const dockerfileRules: DockerfileRule[] = [
164
+ {
165
+ id: "local-inference-pip-ai",
166
+ name: "AI pip packages in Dockerfile",
167
+ category: "local-inference",
168
+ pattern:
169
+ /pip\s+install\s+.*\b(?:torch|tensorflow|transformers|langchain|llama[-_]?cpp|vllm|onnxruntime)\b/,
170
+ confidence: "high",
171
+ },
172
+ {
173
+ id: "local-inference-model-download",
174
+ name: "Model download in Dockerfile",
175
+ category: "local-inference",
176
+ pattern:
177
+ /\b(?:huggingface-cli\s+download|wget\s+.*(?:\.gguf|\.safetensors|\.bin)|COPY\s+.*(?:model|weights))\b/i,
178
+ confidence: "high",
179
+ },
180
+ ];
181
+
182
+ // ---------------------------------------------------------------------------
183
+ // File matching helpers
184
+ // ---------------------------------------------------------------------------
185
+
186
+ function isEnvExample(relativePath: string): boolean {
187
+ const basename = relativePath.split("/").pop() ?? "";
188
+ return /^\.env\.(?:example|sample)$/.test(basename);
189
+ }
190
+
191
+ function isDockerCompose(relativePath: string): boolean {
192
+ const basename = relativePath.split("/").pop() ?? "";
193
+ return /^docker-compose/.test(basename);
194
+ }
195
+
196
+ function isDockerfile(relativePath: string): boolean {
197
+ const basename = relativePath.split("/").pop() ?? "";
198
+ return /^Dockerfile/.test(basename);
199
+ }
200
+
201
+ // ---------------------------------------------------------------------------
202
+ // Detector
203
+ // ---------------------------------------------------------------------------
204
+
205
+ export function detectConfig(ctx: DetectorContext): DetectedSignal[] {
206
+ const { file } = ctx;
207
+ const signals: DetectedSignal[] = [];
208
+
209
+ const lines = ctx.lines;
210
+
211
+ if (isEnvExample(file.relativePath)) {
212
+ detectEnvKeys(file.relativePath, lines, signals);
213
+ } else if (isDockerCompose(file.relativePath)) {
214
+ detectDockerServices(file.relativePath, lines, signals);
215
+ } else if (isDockerfile(file.relativePath)) {
216
+ detectDockerfilePatterns(file.relativePath, lines, signals);
217
+ }
218
+
219
+ return signals;
220
+ }
221
+
222
+ function extractEnvVarName(rawLine: string): string | null {
223
+ const line = rawLine.trim();
224
+ if (!line || line.startsWith("#")) return null;
225
+ const eqIndex = line.indexOf("=");
226
+ return eqIndex >= 0 ? line.slice(0, eqIndex).trim() : line.trim();
227
+ }
228
+
229
+ function detectEnvKeys(relativePath: string, lines: string[], signals: DetectedSignal[]): void {
230
+ for (const rule of envKeyRules) {
231
+ const evidence: SignalEvidence[] = [];
232
+
233
+ for (let i = 0; i < lines.length; i++) {
234
+ const varName = extractEnvVarName(lines[i] ?? "");
235
+ if (varName && rule.pattern.test(varName)) {
236
+ evidence.push({ file: relativePath, line: i + 1, snippet: varName });
237
+ }
238
+ }
239
+
240
+ if (evidence.length > 0) {
241
+ signals.push({
242
+ id: rule.id,
243
+ name: rule.name,
244
+ category: rule.category,
245
+ confidence: rule.confidence,
246
+ evidence,
247
+ });
248
+ }
249
+ }
250
+ }
251
+
252
+ function detectDockerServices(
253
+ relativePath: string,
254
+ lines: string[],
255
+ signals: DetectedSignal[]
256
+ ): void {
257
+ for (const rule of dockerServiceRules) {
258
+ const evidence: SignalEvidence[] = [];
259
+
260
+ for (let i = 0; i < lines.length; i++) {
261
+ const line = lines[i];
262
+ if (!line) continue;
263
+ if (rule.pattern.test(line)) {
264
+ evidence.push({
265
+ file: relativePath,
266
+ line: i + 1,
267
+ snippet: line.trim().slice(0, 120),
268
+ });
269
+ if (evidence.length >= 3) break;
270
+ }
271
+ }
272
+
273
+ if (evidence.length > 0) {
274
+ signals.push({
275
+ id: rule.id,
276
+ name: rule.name,
277
+ category: rule.category,
278
+ confidence: rule.confidence,
279
+ evidence,
280
+ });
281
+ }
282
+ }
283
+ }
284
+
285
+ function detectDockerfilePatterns(
286
+ relativePath: string,
287
+ lines: string[],
288
+ signals: DetectedSignal[]
289
+ ): void {
290
+ for (const rule of dockerfileRules) {
291
+ const evidence: SignalEvidence[] = [];
292
+
293
+ for (let i = 0; i < lines.length; i++) {
294
+ const line = lines[i];
295
+ if (!line) continue;
296
+ if (rule.pattern.test(line)) {
297
+ evidence.push({
298
+ file: relativePath,
299
+ line: i + 1,
300
+ snippet: line.trim().slice(0, 120),
301
+ });
302
+ if (evidence.length >= 3) break;
303
+ }
304
+ }
305
+
306
+ if (evidence.length > 0) {
307
+ signals.push({
308
+ id: rule.id,
309
+ name: rule.name,
310
+ category: rule.category,
311
+ confidence: rule.confidence,
312
+ evidence,
313
+ });
314
+ }
315
+ }
316
+ }