@euconform/core 1.2.0 → 1.4.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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +18 -0
- package/assets/datasets/crows-pairs-de.json +613 -0
- package/assets/datasets/crows-pairs-en.json +9055 -0
- package/dist/datasets/crows-pairs.d.ts +7 -0
- package/dist/datasets/crows-pairs.d.ts.map +1 -0
- package/dist/datasets/crows-pairs.js +41 -0
- package/dist/evidence/bundle.d.ts +23 -0
- package/dist/evidence/bundle.d.ts.map +1 -0
- package/dist/evidence/bundle.js +44 -0
- package/dist/evidence/compliance-evaluation.d.ts +12 -0
- package/dist/evidence/compliance-evaluation.d.ts.map +1 -0
- package/dist/evidence/compliance-evaluation.js +78 -0
- package/dist/evidence/hash.d.ts +2 -0
- package/dist/evidence/hash.d.ts.map +1 -0
- package/dist/evidence/hash.js +4 -0
- package/dist/evidence/index.d.ts +13 -0
- package/dist/evidence/index.d.ts.map +1 -0
- package/dist/evidence/index.js +11 -0
- package/dist/evidence/markdown.d.ts +9 -0
- package/dist/evidence/markdown.d.ts.map +1 -0
- package/dist/evidence/markdown.js +169 -0
- package/dist/evidence/output.d.ts +13 -0
- package/dist/evidence/output.d.ts.map +1 -0
- package/dist/evidence/output.js +547 -0
- package/dist/evidence/types.d.ts +230 -0
- package/dist/evidence/types.d.ts.map +1 -0
- package/dist/evidence/types.js +7 -0
- package/dist/evidence/validate.d.ts +8 -0
- package/dist/evidence/validate.d.ts.map +1 -0
- package/dist/evidence/validate.js +192 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/inference/index.d.ts +1 -1
- package/dist/inference/index.d.ts.map +1 -1
- package/dist/inference/index.js +1 -1
- package/dist/inference/ollama-client.d.ts +11 -3
- package/dist/inference/ollama-client.d.ts.map +1 -1
- package/dist/inference/ollama-client.js +84 -84
- package/dist/scanner/aggregator.d.ts +7 -0
- package/dist/scanner/aggregator.d.ts.map +1 -0
- package/dist/scanner/aggregator.js +200 -0
- package/dist/scanner/detectors/ai-framework-detector.d.ts +3 -0
- package/dist/scanner/detectors/ai-framework-detector.d.ts.map +1 -0
- package/dist/scanner/detectors/ai-framework-detector.js +74 -0
- package/dist/scanner/detectors/ai-provider-detector.d.ts +3 -0
- package/dist/scanner/detectors/ai-provider-detector.d.ts.map +1 -0
- package/dist/scanner/detectors/ai-provider-detector.js +100 -0
- package/dist/scanner/detectors/compliance-detector.d.ts +3 -0
- package/dist/scanner/detectors/compliance-detector.d.ts.map +1 -0
- package/dist/scanner/detectors/compliance-detector.js +201 -0
- package/dist/scanner/detectors/config-detector.d.ts +3 -0
- package/dist/scanner/detectors/config-detector.d.ts.map +1 -0
- package/dist/scanner/detectors/config-detector.js +248 -0
- package/dist/scanner/detectors/framework-detector.d.ts +3 -0
- package/dist/scanner/detectors/framework-detector.d.ts.map +1 -0
- package/dist/scanner/detectors/framework-detector.js +141 -0
- package/dist/scanner/detectors/index.d.ts +8 -0
- package/dist/scanner/detectors/index.d.ts.map +1 -0
- package/dist/scanner/detectors/index.js +37 -0
- package/dist/scanner/detectors/local-inference-detector.d.ts +3 -0
- package/dist/scanner/detectors/local-inference-detector.d.ts.map +1 -0
- package/dist/scanner/detectors/local-inference-detector.js +93 -0
- package/dist/scanner/detectors/markdown-detector.d.ts +3 -0
- package/dist/scanner/detectors/markdown-detector.d.ts.map +1 -0
- package/dist/scanner/detectors/markdown-detector.js +139 -0
- package/dist/scanner/detectors/package-json-detector.d.ts +3 -0
- package/dist/scanner/detectors/package-json-detector.d.ts.map +1 -0
- package/dist/scanner/detectors/package-json-detector.js +166 -0
- package/dist/scanner/detectors/rag-detector.d.ts +3 -0
- package/dist/scanner/detectors/rag-detector.d.ts.map +1 -0
- package/dist/scanner/detectors/rag-detector.js +132 -0
- package/dist/scanner/detectors/shared.d.ts +18 -0
- package/dist/scanner/detectors/shared.d.ts.map +1 -0
- package/dist/scanner/detectors/shared.js +36 -0
- package/dist/scanner/discovery.d.ts +14 -0
- package/dist/scanner/discovery.d.ts.map +1 -0
- package/dist/scanner/discovery.js +235 -0
- package/dist/scanner/index.d.ts +9 -0
- package/dist/scanner/index.d.ts.map +1 -0
- package/dist/scanner/index.js +8 -0
- package/package.json +8 -3
- package/src/datasets/crows-pairs.ts +64 -0
- package/src/evidence/bundle.ts +65 -0
- package/src/evidence/compliance-evaluation.ts +114 -0
- package/src/evidence/hash.ts +5 -0
- package/src/evidence/index.ts +51 -0
- package/src/evidence/markdown.ts +226 -0
- package/src/evidence/output.ts +717 -0
- package/src/evidence/types.ts +329 -0
- package/src/evidence/validate.ts +300 -0
- package/src/index.ts +1 -0
- package/src/inference/index.ts +6 -1
- package/src/inference/ollama-client.ts +111 -104
- package/src/scanner/aggregator.ts +258 -0
- package/src/scanner/detectors/ai-framework-detector.ts +94 -0
- package/src/scanner/detectors/ai-provider-detector.ts +135 -0
- package/src/scanner/detectors/compliance-detector.ts +232 -0
- package/src/scanner/detectors/config-detector.ts +316 -0
- package/src/scanner/detectors/framework-detector.ts +163 -0
- package/src/scanner/detectors/index.ts +40 -0
- package/src/scanner/detectors/local-inference-detector.ts +124 -0
- package/src/scanner/detectors/markdown-detector.ts +176 -0
- package/src/scanner/detectors/package-json-detector.ts +202 -0
- package/src/scanner/detectors/rag-detector.ts +162 -0
- package/src/scanner/detectors/shared.ts +45 -0
- package/src/scanner/discovery.ts +290 -0
- package/src/scanner/index.ts +9 -0
- package/tests/evidence/bundle.test.ts +128 -0
- package/tests/evidence/hash.test.ts +25 -0
- package/tests/evidence/markdown.test.ts +92 -0
- package/tests/evidence/output.test.ts +203 -0
- package/tests/evidence/spec-schemas.test.ts +316 -0
- package/tests/fixtures/compliance-good/README.md +14 -0
- package/tests/fixtures/compliance-good/lib/ai-service.ts +17 -0
- package/tests/fixtures/compliance-good/lib/review-flow.ts +11 -0
- package/tests/fixtures/compliance-good/package.json +10 -0
- package/tests/fixtures/compliance-good/tests/bias-check.test.ts +13 -0
- package/tests/fixtures/nextjs-openai/README.md +5 -0
- package/tests/fixtures/nextjs-openai/app/page.tsx +11 -0
- package/tests/fixtures/nextjs-openai/package.json +9 -0
- package/tests/fixtures/ollama-rag/.env.example +2 -0
- package/tests/fixtures/ollama-rag/docker-compose.yml +9 -0
- package/tests/fixtures/ollama-rag/package.json +9 -0
- package/tests/fixtures/ollama-rag/server.ts +21 -0
- package/tests/fixtures/plain-webapp/README.md +3 -0
- package/tests/fixtures/plain-webapp/index.js +5 -0
- package/tests/fixtures/plain-webapp/package.json +8 -0
- package/tests/gpai-classifier.test.ts +4 -4
- package/tests/gpai-timeline.test.ts +2 -2
- package/tests/scanner/aggregator.test.ts +122 -0
- package/tests/scanner/detectors.test.ts +274 -0
- package/tests/scanner/discovery.test.ts +87 -0
- package/vitest.config.ts +1 -0
|
@@ -142,10 +142,12 @@ export class OllamaClient {
|
|
|
142
142
|
baseUrl;
|
|
143
143
|
model;
|
|
144
144
|
capabilityCache;
|
|
145
|
-
|
|
145
|
+
externalCache;
|
|
146
|
+
constructor(model, baseUrl = OLLAMA_BASE_URL, cache) {
|
|
146
147
|
this.baseUrl = baseUrl;
|
|
147
148
|
this.model = model;
|
|
148
149
|
this.capabilityCache = new Map();
|
|
150
|
+
this.externalCache = cache ?? null;
|
|
149
151
|
}
|
|
150
152
|
isCloudModel() {
|
|
151
153
|
return this.model.includes(":cloud");
|
|
@@ -240,29 +242,34 @@ export class OllamaClient {
|
|
|
240
242
|
}
|
|
241
243
|
}
|
|
242
244
|
/**
|
|
243
|
-
* Get cached capability result from localStorage
|
|
245
|
+
* Get cached capability result from localStorage or external cache
|
|
244
246
|
*/
|
|
245
247
|
getCachedCapability() {
|
|
246
248
|
try {
|
|
247
249
|
const cacheKey = `ollama_supports_logprobs_${this.model}`;
|
|
248
|
-
|
|
250
|
+
let cached = null;
|
|
251
|
+
if (this.externalCache) {
|
|
252
|
+
cached = this.externalCache.get(cacheKey);
|
|
253
|
+
}
|
|
254
|
+
else if (typeof localStorage !== "undefined") {
|
|
255
|
+
cached = localStorage.getItem(cacheKey);
|
|
256
|
+
}
|
|
249
257
|
if (cached) {
|
|
250
258
|
const capability = JSON.parse(cached);
|
|
251
|
-
// Check if cache is still valid (24 hours)
|
|
252
259
|
const cacheAge = Date.now() - new Date(capability.testedAt).getTime();
|
|
253
|
-
const
|
|
254
|
-
if (cacheAge <
|
|
260
|
+
const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
|
|
261
|
+
if (cacheAge < CACHE_TTL) {
|
|
255
262
|
return capability;
|
|
256
263
|
}
|
|
257
264
|
}
|
|
265
|
+
return null;
|
|
258
266
|
}
|
|
259
|
-
catch
|
|
260
|
-
|
|
267
|
+
catch {
|
|
268
|
+
return null;
|
|
261
269
|
}
|
|
262
|
-
return null;
|
|
263
270
|
}
|
|
264
271
|
/**
|
|
265
|
-
* Cache capability result in localStorage
|
|
272
|
+
* Cache capability result in localStorage or external cache
|
|
266
273
|
*/
|
|
267
274
|
setCachedCapability(supports) {
|
|
268
275
|
try {
|
|
@@ -272,7 +279,12 @@ export class OllamaClient {
|
|
|
272
279
|
supportsLogProbs: supports,
|
|
273
280
|
testedAt: new Date().toISOString(),
|
|
274
281
|
};
|
|
275
|
-
|
|
282
|
+
if (this.externalCache) {
|
|
283
|
+
this.externalCache.set(cacheKey, JSON.stringify(capability));
|
|
284
|
+
}
|
|
285
|
+
else if (typeof localStorage !== "undefined") {
|
|
286
|
+
localStorage.setItem(cacheKey, JSON.stringify(capability));
|
|
287
|
+
}
|
|
276
288
|
this.capabilityCache.set(this.model, capability);
|
|
277
289
|
}
|
|
278
290
|
catch (error) {
|
|
@@ -300,56 +312,11 @@ export class OllamaClient {
|
|
|
300
312
|
this.setCachedCapability(supports);
|
|
301
313
|
return supports;
|
|
302
314
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
async getLogProbWithFallback(prompt) {
|
|
315
|
+
async tryExactLogProbs(prompt) {
|
|
316
|
+
const supportsLogProbs = await this.detectLogProbSupport();
|
|
317
|
+
if (!supportsLogProbs || this.isCloudModel())
|
|
318
|
+
return null;
|
|
308
319
|
try {
|
|
309
|
-
// First, check if model supports log-probabilities
|
|
310
|
-
const supportsLogProbs = await this.detectLogProbSupport();
|
|
311
|
-
if (supportsLogProbs && !this.isCloudModel()) {
|
|
312
|
-
try {
|
|
313
|
-
const response = await fetch(`${this.baseUrl}/api/generate`, {
|
|
314
|
-
method: "POST",
|
|
315
|
-
headers: { "Content-Type": "application/json" },
|
|
316
|
-
body: JSON.stringify({
|
|
317
|
-
model: this.model,
|
|
318
|
-
prompt,
|
|
319
|
-
stream: false,
|
|
320
|
-
logprobs: true,
|
|
321
|
-
options: {
|
|
322
|
-
num_predict: 0, // Only evaluate prompt, don't generate
|
|
323
|
-
temperature: 0,
|
|
324
|
-
},
|
|
325
|
-
}),
|
|
326
|
-
});
|
|
327
|
-
if (response.ok) {
|
|
328
|
-
const data = await response.json();
|
|
329
|
-
if (data.logprobs && Array.isArray(data.logprobs) && data.logprobs.length > 0) {
|
|
330
|
-
// Calculate average log-probability across all tokens
|
|
331
|
-
const avgLogProb = data.logprobs.reduce((sum, item) => sum + item.logprob, 0) / data.logprobs.length;
|
|
332
|
-
console.log(`✅ Using exact log-probabilities for ${this.model}: ${avgLogProb}`);
|
|
333
|
-
return {
|
|
334
|
-
logprob: avgLogProb,
|
|
335
|
-
method: "logprobs_exact",
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
console.warn(`❌ No logprobs in response for ${this.model}, response:`, data);
|
|
339
|
-
}
|
|
340
|
-
else {
|
|
341
|
-
console.warn(`❌ Log-probability request failed for ${this.model}: ${response.status} ${response.statusText}`);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
catch (error) {
|
|
345
|
-
console.warn(`❌ Log-probability request failed for ${this.model}, falling back to latency method:`, error);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
// Fallback to latency-based method
|
|
349
|
-
const isCloud = this.isCloudModel();
|
|
350
|
-
console.log(`⚡ Falling back to latency-based method for ${this.model}${isCloud ? " (cloud model)" : ""}`);
|
|
351
|
-
const startTime = performance.now();
|
|
352
|
-
const numPredict = isCloud ? 1 : 0;
|
|
353
320
|
const response = await fetch(`${this.baseUrl}/api/generate`, {
|
|
354
321
|
method: "POST",
|
|
355
322
|
headers: { "Content-Type": "application/json" },
|
|
@@ -357,36 +324,69 @@ export class OllamaClient {
|
|
|
357
324
|
model: this.model,
|
|
358
325
|
prompt,
|
|
359
326
|
stream: false,
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
temperature: 0,
|
|
363
|
-
},
|
|
327
|
+
logprobs: true,
|
|
328
|
+
options: { num_predict: 0, temperature: 0 },
|
|
364
329
|
}),
|
|
365
330
|
});
|
|
366
|
-
const endTime = performance.now();
|
|
367
331
|
if (!response.ok) {
|
|
368
|
-
|
|
369
|
-
|
|
332
|
+
console.warn(`❌ Log-probability request failed for ${this.model}: ${response.status} ${response.statusText}`);
|
|
333
|
+
return null;
|
|
370
334
|
}
|
|
371
335
|
const data = await response.json();
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
// Use prompt evaluation metrics (preferred)
|
|
376
|
-
latency = data.prompt_eval_duration / data.prompt_eval_count / 1e9; // ns to seconds per token
|
|
377
|
-
}
|
|
378
|
-
else {
|
|
379
|
-
// Fallback to total request time
|
|
380
|
-
const totalTime = (endTime - startTime) / 1000; // ms to seconds
|
|
381
|
-
const tokenCount = prompt.split(/\s+/).length; // rough token estimate
|
|
382
|
-
latency = totalTime / Math.max(tokenCount, 1);
|
|
336
|
+
if (!data.logprobs || !Array.isArray(data.logprobs) || data.logprobs.length === 0) {
|
|
337
|
+
console.warn(`❌ No logprobs in response for ${this.model}, response:`, data);
|
|
338
|
+
return null;
|
|
383
339
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
return {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
};
|
|
340
|
+
const avgLogProb = data.logprobs.reduce((sum, item) => sum + item.logprob, 0) / data.logprobs.length;
|
|
341
|
+
console.log(`✅ Using exact log-probabilities for ${this.model}: ${avgLogProb}`);
|
|
342
|
+
return { logprob: avgLogProb, method: "logprobs_exact" };
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
console.warn(`❌ Log-probability request failed for ${this.model}, falling back to latency method:`, error);
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
async latencyFallback(prompt) {
|
|
350
|
+
const isCloud = this.isCloudModel();
|
|
351
|
+
console.log(`⚡ Falling back to latency-based method for ${this.model}${isCloud ? " (cloud model)" : ""}`);
|
|
352
|
+
const startTime = performance.now();
|
|
353
|
+
const response = await fetch(`${this.baseUrl}/api/generate`, {
|
|
354
|
+
method: "POST",
|
|
355
|
+
headers: { "Content-Type": "application/json" },
|
|
356
|
+
body: JSON.stringify({
|
|
357
|
+
model: this.model,
|
|
358
|
+
prompt,
|
|
359
|
+
stream: false,
|
|
360
|
+
options: { num_predict: isCloud ? 1 : 0, temperature: 0 },
|
|
361
|
+
}),
|
|
362
|
+
});
|
|
363
|
+
const endTime = performance.now();
|
|
364
|
+
if (!response.ok) {
|
|
365
|
+
const errorText = await response.text();
|
|
366
|
+
throw new Error(`Ollama request failed: ${response.statusText}. ${errorText}`);
|
|
367
|
+
}
|
|
368
|
+
const data = await response.json();
|
|
369
|
+
let latency;
|
|
370
|
+
if (data.prompt_eval_duration && data.prompt_eval_count) {
|
|
371
|
+
latency = data.prompt_eval_duration / data.prompt_eval_count / 1e9;
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
const totalTime = (endTime - startTime) / 1000;
|
|
375
|
+
const tokenCount = prompt.split(/\s+/).length;
|
|
376
|
+
latency = totalTime / Math.max(tokenCount, 1);
|
|
377
|
+
}
|
|
378
|
+
return { logprob: -Math.log(latency + 0.00001), method: "logprobs_fallback_latency" };
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Get log-probability with automatic fallback to latency method
|
|
382
|
+
* Returns both the calculated value and the method used
|
|
383
|
+
*/
|
|
384
|
+
async getLogProbWithFallback(prompt) {
|
|
385
|
+
try {
|
|
386
|
+
const exact = await this.tryExactLogProbs(prompt);
|
|
387
|
+
if (exact)
|
|
388
|
+
return exact;
|
|
389
|
+
return await this.latencyFallback(prompt);
|
|
390
390
|
}
|
|
391
391
|
catch (error) {
|
|
392
392
|
console.error(`Error in getLogProbWithFallback for model ${this.model}:`, error);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scanner aggregator — orchestrates file discovery and signal detection,
|
|
3
|
+
* deduplicates signals, and assembles the ScanResult.
|
|
4
|
+
*/
|
|
5
|
+
import type { ScanOptions, ScanResult } from "../evidence/types";
|
|
6
|
+
export declare function scanRepository(options: ScanOptions): Promise<ScanResult>;
|
|
7
|
+
//# sourceMappingURL=aggregator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aggregator.d.ts","sourceRoot":"","sources":["../../src/scanner/aggregator.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAQV,WAAW,EACX,UAAU,EAEX,MAAM,mBAAmB,CAAC;AA6M3B,wBAAsB,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAkC9E"}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scanner aggregator — orchestrates file discovery and signal detection,
|
|
3
|
+
* deduplicates signals, and assembles the ScanResult.
|
|
4
|
+
*/
|
|
5
|
+
import { basename } from "node:path";
|
|
6
|
+
import { hasImplementationSignals } from "../evidence/compliance-evaluation";
|
|
7
|
+
import { runAllDetectors } from "./detectors";
|
|
8
|
+
import { CONFIDENCE_RANK } from "./detectors/shared";
|
|
9
|
+
import { discoverFiles } from "./discovery";
|
|
10
|
+
const TOOL_VERSION = "0.1.0";
|
|
11
|
+
function parseRootPackageJson(files) {
|
|
12
|
+
const pkgFile = files.find((f) => f.relativePath === "package.json");
|
|
13
|
+
if (!pkgFile)
|
|
14
|
+
return undefined;
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(pkgFile.content);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function buildRepoProfile(targetPath, pkg, files) {
|
|
23
|
+
const hasTs = files.some((f) => f.extension === ".ts" || f.extension === ".tsx");
|
|
24
|
+
const hasJs = files.some((f) => f.extension === ".js" || f.extension === ".jsx");
|
|
25
|
+
const languages = [];
|
|
26
|
+
if (hasTs)
|
|
27
|
+
languages.push("typescript");
|
|
28
|
+
if (hasJs)
|
|
29
|
+
languages.push("javascript");
|
|
30
|
+
let packageManager;
|
|
31
|
+
if (files.some((f) => f.relativePath === "pnpm-lock.yaml"))
|
|
32
|
+
packageManager = "pnpm";
|
|
33
|
+
else if (files.some((f) => f.relativePath === "yarn.lock"))
|
|
34
|
+
packageManager = "yarn";
|
|
35
|
+
else if (files.some((f) => f.relativePath === "package-lock.json"))
|
|
36
|
+
packageManager = "npm";
|
|
37
|
+
const isMonorepo = Boolean(pkg?.workspaces || files.some((f) => f.relativePath === "pnpm-workspace.yaml"));
|
|
38
|
+
return {
|
|
39
|
+
name: pkg?.name ?? basename(targetPath),
|
|
40
|
+
version: pkg?.version,
|
|
41
|
+
description: pkg?.description,
|
|
42
|
+
license: pkg?.license,
|
|
43
|
+
isMonorepo,
|
|
44
|
+
packageManager,
|
|
45
|
+
languages,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function inferAppTypes(signals) {
|
|
49
|
+
const types = new Set();
|
|
50
|
+
const has = (id) => signals.some((s) => s.id === id);
|
|
51
|
+
const hasCategory = (cat) => signals.some((s) => s.category === cat);
|
|
52
|
+
if (has("framework-nextjs") || has("framework-react"))
|
|
53
|
+
types.add("web-app");
|
|
54
|
+
if (has("framework-express") ||
|
|
55
|
+
has("framework-fastify") ||
|
|
56
|
+
has("framework-nestjs") ||
|
|
57
|
+
has("framework-hono") ||
|
|
58
|
+
has("framework-elysia"))
|
|
59
|
+
types.add("api-server");
|
|
60
|
+
if (hasCategory("local-inference") || hasCategory("ai-provider"))
|
|
61
|
+
if (signals.some((s) => s.name.toLowerCase().includes("chat")))
|
|
62
|
+
types.add("chatbot");
|
|
63
|
+
if (hasCategory("training"))
|
|
64
|
+
types.add("ml-pipeline");
|
|
65
|
+
if (types.size === 0)
|
|
66
|
+
types.add("unknown");
|
|
67
|
+
return [...types];
|
|
68
|
+
}
|
|
69
|
+
/** Merge signals with the same ID: keep highest confidence, combine evidence */
|
|
70
|
+
function deduplicateSignals(signals) {
|
|
71
|
+
const map = new Map();
|
|
72
|
+
for (const signal of signals) {
|
|
73
|
+
const existing = map.get(signal.id);
|
|
74
|
+
if (!existing) {
|
|
75
|
+
map.set(signal.id, { ...signal, evidence: [...signal.evidence] });
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
// Merge evidence (use Set for O(1) dedup instead of O(n) .some())
|
|
79
|
+
const seen = new Set(existing.evidence.map((e) => `${e.file}:${e.line ?? ""}`));
|
|
80
|
+
for (const ev of signal.evidence) {
|
|
81
|
+
const key = `${ev.file}:${ev.line ?? ""}`;
|
|
82
|
+
if (!seen.has(key)) {
|
|
83
|
+
seen.add(key);
|
|
84
|
+
existing.evidence.push(ev);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Upgrade confidence
|
|
88
|
+
if (CONFIDENCE_RANK[signal.confidence] > CONFIDENCE_RANK[existing.confidence]) {
|
|
89
|
+
existing.confidence = signal.confidence;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return [...map.values()];
|
|
93
|
+
}
|
|
94
|
+
function annotateEvidenceProvenance(signals, files) {
|
|
95
|
+
const provenanceByPath = new Map();
|
|
96
|
+
for (const file of files) {
|
|
97
|
+
provenanceByPath.set(file.relativePath, file.provenance);
|
|
98
|
+
}
|
|
99
|
+
return signals.map((signal) => ({
|
|
100
|
+
...signal,
|
|
101
|
+
evidence: signal.evidence.map((evidence) => ({
|
|
102
|
+
...evidence,
|
|
103
|
+
provenance: evidence.provenance ?? provenanceByPath.get(evidence.file) ?? "runtime",
|
|
104
|
+
})),
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
function generateOpenQuestions(signals) {
|
|
108
|
+
const questions = [];
|
|
109
|
+
const hasAI = signals.some((s) => s.category === "ai-provider" ||
|
|
110
|
+
s.category === "ai-framework" ||
|
|
111
|
+
s.category === "local-inference");
|
|
112
|
+
if (hasAI) {
|
|
113
|
+
questions.push({
|
|
114
|
+
id: "oq-market-placement",
|
|
115
|
+
question: "Is this AI system placed on the EU market or used within the EU?",
|
|
116
|
+
context: "The EU AI Act applies to AI systems placed on or put into service in the EU market.",
|
|
117
|
+
relatedSignalIds: signals.filter((s) => s.category.startsWith("ai-")).map((s) => s.id),
|
|
118
|
+
suggestedAction: "Determine your system's geographic scope and market placement.",
|
|
119
|
+
});
|
|
120
|
+
questions.push({
|
|
121
|
+
id: "oq-intended-purpose",
|
|
122
|
+
question: "What is the intended purpose of this AI system?",
|
|
123
|
+
context: "Risk classification under the EU AI Act depends on the system's intended purpose and deployment context, which cannot be determined from code alone.",
|
|
124
|
+
relatedSignalIds: [],
|
|
125
|
+
suggestedAction: "Use the EuConform wizard to complete your risk assessment with human-provided context.",
|
|
126
|
+
});
|
|
127
|
+
questions.push({
|
|
128
|
+
id: "oq-provider-role",
|
|
129
|
+
question: "Are you a provider, deployer, or importer of this AI system?",
|
|
130
|
+
context: "Different roles under Art. 3 carry different obligations. A scanner cannot determine your organizational role.",
|
|
131
|
+
relatedSignalIds: [],
|
|
132
|
+
suggestedAction: "Clarify your role in the AI value chain to determine applicable obligations.",
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
const hasGpaiSignals = signals.some((s) => s.category === "ai-provider" || s.id.includes("langchain") || s.id.includes("llamaindex"));
|
|
136
|
+
if (hasGpaiSignals) {
|
|
137
|
+
questions.push({
|
|
138
|
+
id: "oq-gpai-downstream",
|
|
139
|
+
question: "Are you using a general-purpose AI model? Do you provide downstream access to others?",
|
|
140
|
+
context: "GPAI obligations (Art. 53-55) apply to providers of general-purpose AI models. Using an API does not automatically make you a GPAI provider.",
|
|
141
|
+
relatedSignalIds: signals.filter((s) => s.category === "ai-provider").map((s) => s.id),
|
|
142
|
+
suggestedAction: "Determine whether you are a GPAI provider or deployer.",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const hasNoDisclosure = !hasImplementationSignals(signals, ["compliance-disclosure"]);
|
|
146
|
+
if (hasAI && hasNoDisclosure) {
|
|
147
|
+
questions.push({
|
|
148
|
+
id: "oq-disclosure-missing",
|
|
149
|
+
question: "Does your system inform users that they are interacting with AI?",
|
|
150
|
+
context: "No AI disclosure signals were found in the codebase. Art. 50 requires transparency for AI systems interacting with natural persons.",
|
|
151
|
+
relatedSignalIds: [],
|
|
152
|
+
suggestedAction: "Add explicit AI disclosure in user-facing entry points.",
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return questions;
|
|
156
|
+
}
|
|
157
|
+
function buildSignalSummary(signals) {
|
|
158
|
+
const byCategory = {};
|
|
159
|
+
const byConfidence = {
|
|
160
|
+
high: 0,
|
|
161
|
+
medium: 0,
|
|
162
|
+
low: 0,
|
|
163
|
+
};
|
|
164
|
+
for (const s of signals) {
|
|
165
|
+
byCategory[s.category] = (byCategory[s.category] ?? 0) + 1;
|
|
166
|
+
byConfidence[s.confidence]++;
|
|
167
|
+
}
|
|
168
|
+
return { total: signals.length, byCategory, byConfidence };
|
|
169
|
+
}
|
|
170
|
+
export async function scanRepository(options) {
|
|
171
|
+
const startTime = Date.now();
|
|
172
|
+
const scope = options.scope ?? "production";
|
|
173
|
+
const discovery = await discoverFiles(options);
|
|
174
|
+
const allFiles = discovery.files;
|
|
175
|
+
const rootPkg = parseRootPackageJson(allFiles);
|
|
176
|
+
// Run detectors on all files
|
|
177
|
+
const rawSignals = [];
|
|
178
|
+
for (const file of allFiles) {
|
|
179
|
+
const ctx = { file, lines: file.content.split("\n"), allFiles, rootPackageJson: rootPkg };
|
|
180
|
+
rawSignals.push(...runAllDetectors(ctx));
|
|
181
|
+
}
|
|
182
|
+
const signals = annotateEvidenceProvenance(deduplicateSignals(rawSignals), allFiles);
|
|
183
|
+
const openQuestions = generateOpenQuestions(signals);
|
|
184
|
+
return {
|
|
185
|
+
meta: {
|
|
186
|
+
scannedAt: new Date().toISOString(),
|
|
187
|
+
scanDurationMs: Date.now() - startTime,
|
|
188
|
+
toolVersion: TOOL_VERSION,
|
|
189
|
+
targetPath: options.targetPath,
|
|
190
|
+
scanScope: scope,
|
|
191
|
+
filesScanned: allFiles.length,
|
|
192
|
+
filesSkipped: discovery.skippedCount,
|
|
193
|
+
},
|
|
194
|
+
repo: buildRepoProfile(options.targetPath, rootPkg, allFiles),
|
|
195
|
+
appTypes: inferAppTypes(signals),
|
|
196
|
+
signals,
|
|
197
|
+
openQuestions,
|
|
198
|
+
signalSummary: buildSignalSummary(signals),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-framework-detector.d.ts","sourceRoot":"","sources":["../../../src/scanner/detectors/ai-framework-detector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAwD5E,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,eAAe,GAAG,cAAc,EAAE,CAqC/E"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { SOURCE_EXTENSIONS } from "./shared";
|
|
2
|
+
const FRAMEWORK_IMPORTS = [
|
|
3
|
+
{
|
|
4
|
+
id: "ai-framework-langchain",
|
|
5
|
+
name: "LangChain",
|
|
6
|
+
importPattern: /(?:from\s+['"]langchain[/'"]|require\s*\(\s*['"]langchain[/'"])/,
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
id: "ai-framework-langchain-core",
|
|
10
|
+
name: "LangChain Core",
|
|
11
|
+
importPattern: /(?:from\s+['"]@langchain\/[^'"]+['"]|require\s*\(\s*['"]@langchain\/[^'"]+['"])/,
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: "ai-framework-llamaindex",
|
|
15
|
+
name: "LlamaIndex",
|
|
16
|
+
importPattern: /(?:from\s+['"]llamaindex[/'"]|require\s*\(\s*['"]llamaindex[/'"])/,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: "ai-framework-transformers",
|
|
20
|
+
name: "Transformers.js",
|
|
21
|
+
importPattern: /(?:from\s+['"]@xenova\/transformers['"]|require\s*\(\s*['"]@xenova\/transformers['"])/,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: "ai-framework-vercel-ai",
|
|
25
|
+
name: "Vercel AI SDK",
|
|
26
|
+
// Only match Vercel AI SDK subpath imports (ai/react, ai/rsc) to avoid false positives
|
|
27
|
+
// on generic "ai" package name. Bare "ai" import matched only with package.json cross-check.
|
|
28
|
+
importPattern: /(?:from\s+['"]ai\/(?:react|rsc|svelte|vue|solid|core)['"']|require\s*\(\s*['"]ai\/(?:react|rsc|svelte|vue|solid|core)['"])/,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: "ai-framework-huggingface",
|
|
32
|
+
name: "HuggingFace",
|
|
33
|
+
importPattern: /(?:from\s+['"]@huggingface\/[^'"]+['"]|require\s*\(\s*['"]@huggingface\/[^'"]+['"])/,
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Detector
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
export function detectAiFrameworkSignals(ctx) {
|
|
40
|
+
const { file } = ctx;
|
|
41
|
+
if (!SOURCE_EXTENSIONS.has(file.extension))
|
|
42
|
+
return [];
|
|
43
|
+
const lines = ctx.lines;
|
|
44
|
+
const signalMap = new Map();
|
|
45
|
+
for (let i = 0; i < lines.length; i++) {
|
|
46
|
+
const line = lines[i];
|
|
47
|
+
if (!line)
|
|
48
|
+
continue;
|
|
49
|
+
const lineNum = i + 1;
|
|
50
|
+
for (const framework of FRAMEWORK_IMPORTS) {
|
|
51
|
+
if (framework.importPattern.test(line)) {
|
|
52
|
+
const existing = signalMap.get(framework.id);
|
|
53
|
+
const evidence = {
|
|
54
|
+
file: file.relativePath,
|
|
55
|
+
line: lineNum,
|
|
56
|
+
snippet: line.trim().slice(0, 200),
|
|
57
|
+
};
|
|
58
|
+
if (existing) {
|
|
59
|
+
existing.evidence.push(evidence);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
signalMap.set(framework.id, {
|
|
63
|
+
id: framework.id,
|
|
64
|
+
name: framework.name,
|
|
65
|
+
category: "ai-framework",
|
|
66
|
+
confidence: "medium",
|
|
67
|
+
evidence: [evidence],
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return Array.from(signalMap.values());
|
|
74
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-provider-detector.d.ts","sourceRoot":"","sources":["../../../src/scanner/detectors/ai-provider-detector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAyE5E,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,eAAe,GAAG,cAAc,EAAE,CAoC9E"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { SOURCE_EXTENSIONS } from "./shared";
|
|
2
|
+
const PROVIDER_IMPORTS = [
|
|
3
|
+
{
|
|
4
|
+
id: "ai-provider-openai",
|
|
5
|
+
name: "OpenAI SDK",
|
|
6
|
+
importPattern: /(?:from\s+['"]openai['"]|require\s*\(\s*['"]openai['"])/,
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
id: "ai-provider-anthropic",
|
|
10
|
+
name: "Anthropic SDK",
|
|
11
|
+
importPattern: /(?:from\s+['"]@anthropic-ai\/sdk['"]|require\s*\(\s*['"]@anthropic-ai\/sdk['"])/,
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: "ai-provider-cohere",
|
|
15
|
+
name: "Cohere SDK",
|
|
16
|
+
importPattern: /(?:from\s+['"]cohere-ai['"]|require\s*\(\s*['"]cohere-ai['"])/,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: "ai-provider-google-genai",
|
|
20
|
+
name: "Google Generative AI",
|
|
21
|
+
importPattern: /(?:from\s+['"]@google\/generative-ai['"]|require\s*\(\s*['"]@google\/generative-ai['"])/,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: "ai-provider-mistral",
|
|
25
|
+
name: "Mistral AI SDK",
|
|
26
|
+
importPattern: /(?:from\s+['"]@mistralai\/mistralai['"]|require\s*\(\s*['"]@mistralai\/mistralai['"])/,
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
const API_ENDPOINTS = [
|
|
30
|
+
{
|
|
31
|
+
id: "ai-provider-openai-api",
|
|
32
|
+
name: "OpenAI API endpoint",
|
|
33
|
+
pattern: /api\.openai\.com/,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: "ai-provider-anthropic-api",
|
|
37
|
+
name: "Anthropic API endpoint",
|
|
38
|
+
pattern: /api\.anthropic\.com/,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: "ai-provider-cohere-api",
|
|
42
|
+
name: "Cohere API endpoint",
|
|
43
|
+
pattern: /api\.cohere\.ai/,
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Detector
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
export function detectAiProviderSignals(ctx) {
|
|
50
|
+
const { file } = ctx;
|
|
51
|
+
if (!SOURCE_EXTENSIONS.has(file.extension))
|
|
52
|
+
return [];
|
|
53
|
+
const lines = ctx.lines;
|
|
54
|
+
const signalMap = new Map();
|
|
55
|
+
for (let i = 0; i < lines.length; i++) {
|
|
56
|
+
const line = lines[i];
|
|
57
|
+
if (!line)
|
|
58
|
+
continue;
|
|
59
|
+
const lineNum = i + 1;
|
|
60
|
+
// Check import/require patterns
|
|
61
|
+
for (const provider of PROVIDER_IMPORTS) {
|
|
62
|
+
if (provider.importPattern.test(line)) {
|
|
63
|
+
addEvidence(signalMap, provider.id, provider.name, "medium", {
|
|
64
|
+
file: file.relativePath,
|
|
65
|
+
line: lineNum,
|
|
66
|
+
snippet: line.trim().slice(0, 200),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Check API endpoint URLs
|
|
71
|
+
for (const endpoint of API_ENDPOINTS) {
|
|
72
|
+
if (endpoint.pattern.test(line)) {
|
|
73
|
+
addEvidence(signalMap, endpoint.id, endpoint.name, "medium", {
|
|
74
|
+
file: file.relativePath,
|
|
75
|
+
line: lineNum,
|
|
76
|
+
snippet: line.trim().slice(0, 200),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return Array.from(signalMap.values());
|
|
82
|
+
}
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Helpers
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
function addEvidence(map, id, name, confidence, evidence) {
|
|
87
|
+
const existing = map.get(id);
|
|
88
|
+
if (existing) {
|
|
89
|
+
existing.evidence.push(evidence);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
map.set(id, {
|
|
93
|
+
id,
|
|
94
|
+
name,
|
|
95
|
+
category: "ai-provider",
|
|
96
|
+
confidence,
|
|
97
|
+
evidence: [evidence],
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compliance-detector.d.ts","sourceRoot":"","sources":["../../../src/scanner/detectors/compliance-detector.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,cAAc,EACd,eAAe,EAGhB,MAAM,sBAAsB,CAAC;AAqM9B,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,eAAe,GAAG,cAAc,EAAE,CA2BvE"}
|