@euconform/cli 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.
- package/LICENSE +21 -0
- package/dist/index.js +4093 -0
- package/package.json +54 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4093 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
9
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
|
|
28
|
+
// ../../node_modules/.pnpm/ignore@7.0.5/node_modules/ignore/index.js
|
|
29
|
+
var require_ignore = __commonJS({
|
|
30
|
+
"../../node_modules/.pnpm/ignore@7.0.5/node_modules/ignore/index.js"(exports, module) {
|
|
31
|
+
"use strict";
|
|
32
|
+
function makeArray(subject) {
|
|
33
|
+
return Array.isArray(subject) ? subject : [subject];
|
|
34
|
+
}
|
|
35
|
+
var UNDEFINED = void 0;
|
|
36
|
+
var EMPTY = "";
|
|
37
|
+
var SPACE = " ";
|
|
38
|
+
var ESCAPE = "\\";
|
|
39
|
+
var REGEX_TEST_BLANK_LINE = /^\s+$/;
|
|
40
|
+
var REGEX_INVALID_TRAILING_BACKSLASH = /(?:[^\\]|^)\\$/;
|
|
41
|
+
var REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION = /^\\!/;
|
|
42
|
+
var REGEX_REPLACE_LEADING_EXCAPED_HASH = /^\\#/;
|
|
43
|
+
var REGEX_SPLITALL_CRLF = /\r?\n/g;
|
|
44
|
+
var REGEX_TEST_INVALID_PATH = /^\.{0,2}\/|^\.{1,2}$/;
|
|
45
|
+
var REGEX_TEST_TRAILING_SLASH = /\/$/;
|
|
46
|
+
var SLASH = "/";
|
|
47
|
+
var TMP_KEY_IGNORE = "node-ignore";
|
|
48
|
+
if (typeof Symbol !== "undefined") {
|
|
49
|
+
TMP_KEY_IGNORE = /* @__PURE__ */ Symbol.for("node-ignore");
|
|
50
|
+
}
|
|
51
|
+
var KEY_IGNORE = TMP_KEY_IGNORE;
|
|
52
|
+
var define = (object, key, value) => {
|
|
53
|
+
Object.defineProperty(object, key, { value });
|
|
54
|
+
return value;
|
|
55
|
+
};
|
|
56
|
+
var REGEX_REGEXP_RANGE = /([0-z])-([0-z])/g;
|
|
57
|
+
var RETURN_FALSE = () => false;
|
|
58
|
+
var sanitizeRange = (range) => range.replace(
|
|
59
|
+
REGEX_REGEXP_RANGE,
|
|
60
|
+
(match, from, to) => from.charCodeAt(0) <= to.charCodeAt(0) ? match : EMPTY
|
|
61
|
+
);
|
|
62
|
+
var cleanRangeBackSlash = (slashes) => {
|
|
63
|
+
const { length } = slashes;
|
|
64
|
+
return slashes.slice(0, length - length % 2);
|
|
65
|
+
};
|
|
66
|
+
var REPLACERS = [
|
|
67
|
+
[
|
|
68
|
+
// Remove BOM
|
|
69
|
+
// TODO:
|
|
70
|
+
// Other similar zero-width characters?
|
|
71
|
+
/^\uFEFF/,
|
|
72
|
+
() => EMPTY
|
|
73
|
+
],
|
|
74
|
+
// > Trailing spaces are ignored unless they are quoted with backslash ("\")
|
|
75
|
+
[
|
|
76
|
+
// (a\ ) -> (a )
|
|
77
|
+
// (a ) -> (a)
|
|
78
|
+
// (a ) -> (a)
|
|
79
|
+
// (a \ ) -> (a )
|
|
80
|
+
/((?:\\\\)*?)(\\?\s+)$/,
|
|
81
|
+
(_, m1, m2) => m1 + (m2.indexOf("\\") === 0 ? SPACE : EMPTY)
|
|
82
|
+
],
|
|
83
|
+
// Replace (\ ) with ' '
|
|
84
|
+
// (\ ) -> ' '
|
|
85
|
+
// (\\ ) -> '\\ '
|
|
86
|
+
// (\\\ ) -> '\\ '
|
|
87
|
+
[
|
|
88
|
+
/(\\+?)\s/g,
|
|
89
|
+
(_, m1) => {
|
|
90
|
+
const { length } = m1;
|
|
91
|
+
return m1.slice(0, length - length % 2) + SPACE;
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
// Escape metacharacters
|
|
95
|
+
// which is written down by users but means special for regular expressions.
|
|
96
|
+
// > There are 12 characters with special meanings:
|
|
97
|
+
// > - the backslash \,
|
|
98
|
+
// > - the caret ^,
|
|
99
|
+
// > - the dollar sign $,
|
|
100
|
+
// > - the period or dot .,
|
|
101
|
+
// > - the vertical bar or pipe symbol |,
|
|
102
|
+
// > - the question mark ?,
|
|
103
|
+
// > - the asterisk or star *,
|
|
104
|
+
// > - the plus sign +,
|
|
105
|
+
// > - the opening parenthesis (,
|
|
106
|
+
// > - the closing parenthesis ),
|
|
107
|
+
// > - and the opening square bracket [,
|
|
108
|
+
// > - the opening curly brace {,
|
|
109
|
+
// > These special characters are often called "metacharacters".
|
|
110
|
+
[
|
|
111
|
+
/[\\$.|*+(){^]/g,
|
|
112
|
+
(match) => `\\${match}`
|
|
113
|
+
],
|
|
114
|
+
[
|
|
115
|
+
// > a question mark (?) matches a single character
|
|
116
|
+
/(?!\\)\?/g,
|
|
117
|
+
() => "[^/]"
|
|
118
|
+
],
|
|
119
|
+
// leading slash
|
|
120
|
+
[
|
|
121
|
+
// > A leading slash matches the beginning of the pathname.
|
|
122
|
+
// > For example, "/*.c" matches "cat-file.c" but not "mozilla-sha1/sha1.c".
|
|
123
|
+
// A leading slash matches the beginning of the pathname
|
|
124
|
+
/^\//,
|
|
125
|
+
() => "^"
|
|
126
|
+
],
|
|
127
|
+
// replace special metacharacter slash after the leading slash
|
|
128
|
+
[
|
|
129
|
+
/\//g,
|
|
130
|
+
() => "\\/"
|
|
131
|
+
],
|
|
132
|
+
[
|
|
133
|
+
// > A leading "**" followed by a slash means match in all directories.
|
|
134
|
+
// > For example, "**/foo" matches file or directory "foo" anywhere,
|
|
135
|
+
// > the same as pattern "foo".
|
|
136
|
+
// > "**/foo/bar" matches file or directory "bar" anywhere that is directly
|
|
137
|
+
// > under directory "foo".
|
|
138
|
+
// Notice that the '*'s have been replaced as '\\*'
|
|
139
|
+
/^\^*\\\*\\\*\\\//,
|
|
140
|
+
// '**/foo' <-> 'foo'
|
|
141
|
+
() => "^(?:.*\\/)?"
|
|
142
|
+
],
|
|
143
|
+
// starting
|
|
144
|
+
[
|
|
145
|
+
// there will be no leading '/'
|
|
146
|
+
// (which has been replaced by section "leading slash")
|
|
147
|
+
// If starts with '**', adding a '^' to the regular expression also works
|
|
148
|
+
/^(?=[^^])/,
|
|
149
|
+
function startingReplacer() {
|
|
150
|
+
return !/\/(?!$)/.test(this) ? "(?:^|\\/)" : "^";
|
|
151
|
+
}
|
|
152
|
+
],
|
|
153
|
+
// two globstars
|
|
154
|
+
[
|
|
155
|
+
// Use lookahead assertions so that we could match more than one `'/**'`
|
|
156
|
+
/\\\/\\\*\\\*(?=\\\/|$)/g,
|
|
157
|
+
// Zero, one or several directories
|
|
158
|
+
// should not use '*', or it will be replaced by the next replacer
|
|
159
|
+
// Check if it is not the last `'/**'`
|
|
160
|
+
(_, index, str) => index + 6 < str.length ? "(?:\\/[^\\/]+)*" : "\\/.+"
|
|
161
|
+
],
|
|
162
|
+
// normal intermediate wildcards
|
|
163
|
+
[
|
|
164
|
+
// Never replace escaped '*'
|
|
165
|
+
// ignore rule '\*' will match the path '*'
|
|
166
|
+
// 'abc.*/' -> go
|
|
167
|
+
// 'abc.*' -> skip this rule,
|
|
168
|
+
// coz trailing single wildcard will be handed by [trailing wildcard]
|
|
169
|
+
/(^|[^\\]+)(\\\*)+(?=.+)/g,
|
|
170
|
+
// '*.js' matches '.js'
|
|
171
|
+
// '*.js' doesn't match 'abc'
|
|
172
|
+
(_, p1, p2) => {
|
|
173
|
+
const unescaped = p2.replace(/\\\*/g, "[^\\/]*");
|
|
174
|
+
return p1 + unescaped;
|
|
175
|
+
}
|
|
176
|
+
],
|
|
177
|
+
[
|
|
178
|
+
// unescape, revert step 3 except for back slash
|
|
179
|
+
// For example, if a user escape a '\\*',
|
|
180
|
+
// after step 3, the result will be '\\\\\\*'
|
|
181
|
+
/\\\\\\(?=[$.|*+(){^])/g,
|
|
182
|
+
() => ESCAPE
|
|
183
|
+
],
|
|
184
|
+
[
|
|
185
|
+
// '\\\\' -> '\\'
|
|
186
|
+
/\\\\/g,
|
|
187
|
+
() => ESCAPE
|
|
188
|
+
],
|
|
189
|
+
[
|
|
190
|
+
// > The range notation, e.g. [a-zA-Z],
|
|
191
|
+
// > can be used to match one of the characters in a range.
|
|
192
|
+
// `\` is escaped by step 3
|
|
193
|
+
/(\\)?\[([^\]/]*?)(\\*)($|\])/g,
|
|
194
|
+
(match, leadEscape, range, endEscape, close) => leadEscape === ESCAPE ? `\\[${range}${cleanRangeBackSlash(endEscape)}${close}` : close === "]" ? endEscape.length % 2 === 0 ? `[${sanitizeRange(range)}${endEscape}]` : "[]" : "[]"
|
|
195
|
+
],
|
|
196
|
+
// ending
|
|
197
|
+
[
|
|
198
|
+
// 'js' will not match 'js.'
|
|
199
|
+
// 'ab' will not match 'abc'
|
|
200
|
+
/(?:[^*])$/,
|
|
201
|
+
// WTF!
|
|
202
|
+
// https://git-scm.com/docs/gitignore
|
|
203
|
+
// changes in [2.22.1](https://git-scm.com/docs/gitignore/2.22.1)
|
|
204
|
+
// which re-fixes #24, #38
|
|
205
|
+
// > If there is a separator at the end of the pattern then the pattern
|
|
206
|
+
// > will only match directories, otherwise the pattern can match both
|
|
207
|
+
// > files and directories.
|
|
208
|
+
// 'js*' will not match 'a.js'
|
|
209
|
+
// 'js/' will not match 'a.js'
|
|
210
|
+
// 'js' will match 'a.js' and 'a.js/'
|
|
211
|
+
(match) => /\/$/.test(match) ? `${match}$` : `${match}(?=$|\\/$)`
|
|
212
|
+
]
|
|
213
|
+
];
|
|
214
|
+
var REGEX_REPLACE_TRAILING_WILDCARD = /(^|\\\/)?\\\*$/;
|
|
215
|
+
var MODE_IGNORE = "regex";
|
|
216
|
+
var MODE_CHECK_IGNORE = "checkRegex";
|
|
217
|
+
var UNDERSCORE = "_";
|
|
218
|
+
var TRAILING_WILD_CARD_REPLACERS = {
|
|
219
|
+
[MODE_IGNORE](_, p1) {
|
|
220
|
+
const prefix = p1 ? `${p1}[^/]+` : "[^/]*";
|
|
221
|
+
return `${prefix}(?=$|\\/$)`;
|
|
222
|
+
},
|
|
223
|
+
[MODE_CHECK_IGNORE](_, p1) {
|
|
224
|
+
const prefix = p1 ? `${p1}[^/]*` : "[^/]*";
|
|
225
|
+
return `${prefix}(?=$|\\/$)`;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
var makeRegexPrefix = (pattern) => REPLACERS.reduce(
|
|
229
|
+
(prev, [matcher, replacer]) => prev.replace(matcher, replacer.bind(pattern)),
|
|
230
|
+
pattern
|
|
231
|
+
);
|
|
232
|
+
var isString = (subject) => typeof subject === "string";
|
|
233
|
+
var checkPattern = (pattern) => pattern && isString(pattern) && !REGEX_TEST_BLANK_LINE.test(pattern) && !REGEX_INVALID_TRAILING_BACKSLASH.test(pattern) && pattern.indexOf("#") !== 0;
|
|
234
|
+
var splitPattern = (pattern) => pattern.split(REGEX_SPLITALL_CRLF).filter(Boolean);
|
|
235
|
+
var IgnoreRule = class {
|
|
236
|
+
constructor(pattern, mark, body, ignoreCase, negative, prefix) {
|
|
237
|
+
this.pattern = pattern;
|
|
238
|
+
this.mark = mark;
|
|
239
|
+
this.negative = negative;
|
|
240
|
+
define(this, "body", body);
|
|
241
|
+
define(this, "ignoreCase", ignoreCase);
|
|
242
|
+
define(this, "regexPrefix", prefix);
|
|
243
|
+
}
|
|
244
|
+
get regex() {
|
|
245
|
+
const key = UNDERSCORE + MODE_IGNORE;
|
|
246
|
+
if (this[key]) {
|
|
247
|
+
return this[key];
|
|
248
|
+
}
|
|
249
|
+
return this._make(MODE_IGNORE, key);
|
|
250
|
+
}
|
|
251
|
+
get checkRegex() {
|
|
252
|
+
const key = UNDERSCORE + MODE_CHECK_IGNORE;
|
|
253
|
+
if (this[key]) {
|
|
254
|
+
return this[key];
|
|
255
|
+
}
|
|
256
|
+
return this._make(MODE_CHECK_IGNORE, key);
|
|
257
|
+
}
|
|
258
|
+
_make(mode, key) {
|
|
259
|
+
const str = this.regexPrefix.replace(
|
|
260
|
+
REGEX_REPLACE_TRAILING_WILDCARD,
|
|
261
|
+
// It does not need to bind pattern
|
|
262
|
+
TRAILING_WILD_CARD_REPLACERS[mode]
|
|
263
|
+
);
|
|
264
|
+
const regex = this.ignoreCase ? new RegExp(str, "i") : new RegExp(str);
|
|
265
|
+
return define(this, key, regex);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
var createRule = ({
|
|
269
|
+
pattern,
|
|
270
|
+
mark
|
|
271
|
+
}, ignoreCase) => {
|
|
272
|
+
let negative = false;
|
|
273
|
+
let body = pattern;
|
|
274
|
+
if (body.indexOf("!") === 0) {
|
|
275
|
+
negative = true;
|
|
276
|
+
body = body.substr(1);
|
|
277
|
+
}
|
|
278
|
+
body = body.replace(REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION, "!").replace(REGEX_REPLACE_LEADING_EXCAPED_HASH, "#");
|
|
279
|
+
const regexPrefix = makeRegexPrefix(body);
|
|
280
|
+
return new IgnoreRule(
|
|
281
|
+
pattern,
|
|
282
|
+
mark,
|
|
283
|
+
body,
|
|
284
|
+
ignoreCase,
|
|
285
|
+
negative,
|
|
286
|
+
regexPrefix
|
|
287
|
+
);
|
|
288
|
+
};
|
|
289
|
+
var RuleManager = class {
|
|
290
|
+
constructor(ignoreCase) {
|
|
291
|
+
this._ignoreCase = ignoreCase;
|
|
292
|
+
this._rules = [];
|
|
293
|
+
}
|
|
294
|
+
_add(pattern) {
|
|
295
|
+
if (pattern && pattern[KEY_IGNORE]) {
|
|
296
|
+
this._rules = this._rules.concat(pattern._rules._rules);
|
|
297
|
+
this._added = true;
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (isString(pattern)) {
|
|
301
|
+
pattern = {
|
|
302
|
+
pattern
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
if (checkPattern(pattern.pattern)) {
|
|
306
|
+
const rule = createRule(pattern, this._ignoreCase);
|
|
307
|
+
this._added = true;
|
|
308
|
+
this._rules.push(rule);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// @param {Array<string> | string | Ignore} pattern
|
|
312
|
+
add(pattern) {
|
|
313
|
+
this._added = false;
|
|
314
|
+
makeArray(
|
|
315
|
+
isString(pattern) ? splitPattern(pattern) : pattern
|
|
316
|
+
).forEach(this._add, this);
|
|
317
|
+
return this._added;
|
|
318
|
+
}
|
|
319
|
+
// Test one single path without recursively checking parent directories
|
|
320
|
+
//
|
|
321
|
+
// - checkUnignored `boolean` whether should check if the path is unignored,
|
|
322
|
+
// setting `checkUnignored` to `false` could reduce additional
|
|
323
|
+
// path matching.
|
|
324
|
+
// - check `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`
|
|
325
|
+
// @returns {TestResult} true if a file is ignored
|
|
326
|
+
test(path, checkUnignored, mode) {
|
|
327
|
+
let ignored = false;
|
|
328
|
+
let unignored = false;
|
|
329
|
+
let matchedRule;
|
|
330
|
+
this._rules.forEach((rule) => {
|
|
331
|
+
const { negative } = rule;
|
|
332
|
+
if (unignored === negative && ignored !== unignored || negative && !ignored && !unignored && !checkUnignored) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const matched = rule[mode].test(path);
|
|
336
|
+
if (!matched) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
ignored = !negative;
|
|
340
|
+
unignored = negative;
|
|
341
|
+
matchedRule = negative ? UNDEFINED : rule;
|
|
342
|
+
});
|
|
343
|
+
const ret = {
|
|
344
|
+
ignored,
|
|
345
|
+
unignored
|
|
346
|
+
};
|
|
347
|
+
if (matchedRule) {
|
|
348
|
+
ret.rule = matchedRule;
|
|
349
|
+
}
|
|
350
|
+
return ret;
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
var throwError = (message, Ctor) => {
|
|
354
|
+
throw new Ctor(message);
|
|
355
|
+
};
|
|
356
|
+
var checkPath = (path, originalPath, doThrow) => {
|
|
357
|
+
if (!isString(path)) {
|
|
358
|
+
return doThrow(
|
|
359
|
+
`path must be a string, but got \`${originalPath}\``,
|
|
360
|
+
TypeError
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
if (!path) {
|
|
364
|
+
return doThrow(`path must not be empty`, TypeError);
|
|
365
|
+
}
|
|
366
|
+
if (checkPath.isNotRelative(path)) {
|
|
367
|
+
const r = "`path.relative()`d";
|
|
368
|
+
return doThrow(
|
|
369
|
+
`path should be a ${r} string, but got "${originalPath}"`,
|
|
370
|
+
RangeError
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
return true;
|
|
374
|
+
};
|
|
375
|
+
var isNotRelative = (path) => REGEX_TEST_INVALID_PATH.test(path);
|
|
376
|
+
checkPath.isNotRelative = isNotRelative;
|
|
377
|
+
checkPath.convert = (p) => p;
|
|
378
|
+
var Ignore = class {
|
|
379
|
+
constructor({
|
|
380
|
+
ignorecase = true,
|
|
381
|
+
ignoreCase = ignorecase,
|
|
382
|
+
allowRelativePaths = false
|
|
383
|
+
} = {}) {
|
|
384
|
+
define(this, KEY_IGNORE, true);
|
|
385
|
+
this._rules = new RuleManager(ignoreCase);
|
|
386
|
+
this._strictPathCheck = !allowRelativePaths;
|
|
387
|
+
this._initCache();
|
|
388
|
+
}
|
|
389
|
+
_initCache() {
|
|
390
|
+
this._ignoreCache = /* @__PURE__ */ Object.create(null);
|
|
391
|
+
this._testCache = /* @__PURE__ */ Object.create(null);
|
|
392
|
+
}
|
|
393
|
+
add(pattern) {
|
|
394
|
+
if (this._rules.add(pattern)) {
|
|
395
|
+
this._initCache();
|
|
396
|
+
}
|
|
397
|
+
return this;
|
|
398
|
+
}
|
|
399
|
+
// legacy
|
|
400
|
+
addPattern(pattern) {
|
|
401
|
+
return this.add(pattern);
|
|
402
|
+
}
|
|
403
|
+
// @returns {TestResult}
|
|
404
|
+
_test(originalPath, cache, checkUnignored, slices) {
|
|
405
|
+
const path = originalPath && checkPath.convert(originalPath);
|
|
406
|
+
checkPath(
|
|
407
|
+
path,
|
|
408
|
+
originalPath,
|
|
409
|
+
this._strictPathCheck ? throwError : RETURN_FALSE
|
|
410
|
+
);
|
|
411
|
+
return this._t(path, cache, checkUnignored, slices);
|
|
412
|
+
}
|
|
413
|
+
checkIgnore(path) {
|
|
414
|
+
if (!REGEX_TEST_TRAILING_SLASH.test(path)) {
|
|
415
|
+
return this.test(path);
|
|
416
|
+
}
|
|
417
|
+
const slices = path.split(SLASH).filter(Boolean);
|
|
418
|
+
slices.pop();
|
|
419
|
+
if (slices.length) {
|
|
420
|
+
const parent = this._t(
|
|
421
|
+
slices.join(SLASH) + SLASH,
|
|
422
|
+
this._testCache,
|
|
423
|
+
true,
|
|
424
|
+
slices
|
|
425
|
+
);
|
|
426
|
+
if (parent.ignored) {
|
|
427
|
+
return parent;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return this._rules.test(path, false, MODE_CHECK_IGNORE);
|
|
431
|
+
}
|
|
432
|
+
_t(path, cache, checkUnignored, slices) {
|
|
433
|
+
if (path in cache) {
|
|
434
|
+
return cache[path];
|
|
435
|
+
}
|
|
436
|
+
if (!slices) {
|
|
437
|
+
slices = path.split(SLASH).filter(Boolean);
|
|
438
|
+
}
|
|
439
|
+
slices.pop();
|
|
440
|
+
if (!slices.length) {
|
|
441
|
+
return cache[path] = this._rules.test(path, checkUnignored, MODE_IGNORE);
|
|
442
|
+
}
|
|
443
|
+
const parent = this._t(
|
|
444
|
+
slices.join(SLASH) + SLASH,
|
|
445
|
+
cache,
|
|
446
|
+
checkUnignored,
|
|
447
|
+
slices
|
|
448
|
+
);
|
|
449
|
+
return cache[path] = parent.ignored ? parent : this._rules.test(path, checkUnignored, MODE_IGNORE);
|
|
450
|
+
}
|
|
451
|
+
ignores(path) {
|
|
452
|
+
return this._test(path, this._ignoreCache, false).ignored;
|
|
453
|
+
}
|
|
454
|
+
createFilter() {
|
|
455
|
+
return (path) => !this.ignores(path);
|
|
456
|
+
}
|
|
457
|
+
filter(paths) {
|
|
458
|
+
return makeArray(paths).filter(this.createFilter());
|
|
459
|
+
}
|
|
460
|
+
// @returns {TestResult}
|
|
461
|
+
test(path) {
|
|
462
|
+
return this._test(path, this._testCache, true);
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
var factory = (options) => new Ignore(options);
|
|
466
|
+
var isPathValid = (path) => checkPath(path && checkPath.convert(path), path, RETURN_FALSE);
|
|
467
|
+
var setupWindows = () => {
|
|
468
|
+
const makePosix = (str) => /^\\\\\?\\/.test(str) || /["<>|\u0000-\u001F]+/u.test(str) ? str : str.replace(/\\/g, "/");
|
|
469
|
+
checkPath.convert = makePosix;
|
|
470
|
+
const REGEX_TEST_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i;
|
|
471
|
+
checkPath.isNotRelative = (path) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path) || isNotRelative(path);
|
|
472
|
+
};
|
|
473
|
+
if (
|
|
474
|
+
// Detect `process` so that it can run in browsers.
|
|
475
|
+
typeof process !== "undefined" && process.platform === "win32"
|
|
476
|
+
) {
|
|
477
|
+
setupWindows();
|
|
478
|
+
}
|
|
479
|
+
module.exports = factory;
|
|
480
|
+
factory.default = factory;
|
|
481
|
+
module.exports.isPathValid = isPathValid;
|
|
482
|
+
define(module.exports, /* @__PURE__ */ Symbol.for("setupWindows"), setupWindows);
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// src/index.ts
|
|
487
|
+
import { defineCommand as defineCommand3, runMain } from "citty";
|
|
488
|
+
|
|
489
|
+
// src/commands/scan.ts
|
|
490
|
+
import { stat as stat2 } from "fs/promises";
|
|
491
|
+
import { resolve as resolve2 } from "path";
|
|
492
|
+
|
|
493
|
+
// ../core/src/evidence/compliance-evaluation.ts
|
|
494
|
+
var IMPLEMENTATION_PROVENANCE = /* @__PURE__ */ new Set(["runtime", "config"]);
|
|
495
|
+
var SUPPORTING_PROVENANCE = /* @__PURE__ */ new Set(["root-docs", "docs"]);
|
|
496
|
+
var USER_FACING_SEGMENTS = /* @__PURE__ */ new Set([
|
|
497
|
+
"app",
|
|
498
|
+
"pages",
|
|
499
|
+
"routes",
|
|
500
|
+
"route",
|
|
501
|
+
"components",
|
|
502
|
+
"ui",
|
|
503
|
+
"views",
|
|
504
|
+
"view",
|
|
505
|
+
"screens",
|
|
506
|
+
"screen",
|
|
507
|
+
"templates",
|
|
508
|
+
"template",
|
|
509
|
+
"public",
|
|
510
|
+
"content",
|
|
511
|
+
"locales",
|
|
512
|
+
"messages",
|
|
513
|
+
"copy",
|
|
514
|
+
"emails"
|
|
515
|
+
]);
|
|
516
|
+
var USER_FACING_BASENAME_PATTERN = /(?:page|layout|route|template|loading|error|view|screen|dialog|modal|form|widget|banner|notice)\.[^.]+$/i;
|
|
517
|
+
function normalizedProvenance(provenance) {
|
|
518
|
+
return provenance ?? "runtime";
|
|
519
|
+
}
|
|
520
|
+
function isInternalAnalysisPath(relativePath) {
|
|
521
|
+
const normalized = relativePath.replace(/\\/g, "/").toLowerCase();
|
|
522
|
+
return normalized.includes("/scanner/") || normalized.includes("/detectors/");
|
|
523
|
+
}
|
|
524
|
+
function isUserFacingPath(relativePath) {
|
|
525
|
+
const normalized = relativePath.replace(/\\/g, "/").toLowerCase();
|
|
526
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
527
|
+
const basename4 = segments[segments.length - 1] ?? "";
|
|
528
|
+
return segments.some((segment) => USER_FACING_SEGMENTS.has(segment)) || USER_FACING_BASENAME_PATTERN.test(basename4);
|
|
529
|
+
}
|
|
530
|
+
function isImplementationEvidence(category, evidence) {
|
|
531
|
+
const provenance = normalizedProvenance(evidence.provenance);
|
|
532
|
+
if (!IMPLEMENTATION_PROVENANCE.has(provenance)) return false;
|
|
533
|
+
if (isInternalAnalysisPath(evidence.file)) return false;
|
|
534
|
+
if (category === "compliance-disclosure") {
|
|
535
|
+
return isUserFacingPath(evidence.file);
|
|
536
|
+
}
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
function isSupportingEvidence(category, evidence) {
|
|
540
|
+
const provenance = normalizedProvenance(evidence.provenance);
|
|
541
|
+
if (!SUPPORTING_PROVENANCE.has(provenance)) return false;
|
|
542
|
+
if (category === "compliance-disclosure") return true;
|
|
543
|
+
if (category.startsWith("compliance-")) return true;
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
function withFilteredEvidence(signal, matcher) {
|
|
547
|
+
const evidence = signal.evidence.filter(matcher);
|
|
548
|
+
if (evidence.length === 0) return null;
|
|
549
|
+
return { ...signal, evidence };
|
|
550
|
+
}
|
|
551
|
+
function evaluateSignalsForCategories(signals, categories) {
|
|
552
|
+
const relevant = signals.filter((signal) => categories.includes(signal.category));
|
|
553
|
+
return {
|
|
554
|
+
implementationSignals: relevant.map(
|
|
555
|
+
(signal) => withFilteredEvidence(
|
|
556
|
+
signal,
|
|
557
|
+
(evidence) => isImplementationEvidence(signal.category, evidence)
|
|
558
|
+
)
|
|
559
|
+
).filter((signal) => signal !== null),
|
|
560
|
+
supportingSignals: relevant.map(
|
|
561
|
+
(signal) => withFilteredEvidence(signal, (evidence) => isSupportingEvidence(signal.category, evidence))
|
|
562
|
+
).filter((signal) => signal !== null)
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
function hasImplementationSignals(signals, categories) {
|
|
566
|
+
return evaluateSignalsForCategories(signals, categories).implementationSignals.length > 0;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// ../core/src/evidence/markdown.ts
|
|
570
|
+
var COMPLIANCE_LABELS = {
|
|
571
|
+
disclosure: "AI Disclosure",
|
|
572
|
+
biasTesting: "Bias Testing",
|
|
573
|
+
reportingExports: "Reporting & Exports",
|
|
574
|
+
loggingMonitoring: "Logging & Monitoring",
|
|
575
|
+
humanOversight: "Human Oversight",
|
|
576
|
+
dataGovernance: "Data Governance",
|
|
577
|
+
incidentReporting: "Incident Reporting"
|
|
578
|
+
};
|
|
579
|
+
var STATUS_LABELS = {
|
|
580
|
+
present: "Present",
|
|
581
|
+
partial: "Partial",
|
|
582
|
+
absent: "Absent",
|
|
583
|
+
unknown: "Unknown"
|
|
584
|
+
};
|
|
585
|
+
function bomKindLabel(kind) {
|
|
586
|
+
const labels = {
|
|
587
|
+
framework: "Framework",
|
|
588
|
+
runtime: "Runtime",
|
|
589
|
+
"inference-provider": "Inference Provider",
|
|
590
|
+
"ai-framework": "AI Framework",
|
|
591
|
+
model: "Model",
|
|
592
|
+
"vector-store": "Vector Store",
|
|
593
|
+
embedding: "Embedding",
|
|
594
|
+
dataset: "Dataset",
|
|
595
|
+
tool: "Tool"
|
|
596
|
+
};
|
|
597
|
+
return labels[kind];
|
|
598
|
+
}
|
|
599
|
+
function formatDate(iso) {
|
|
600
|
+
try {
|
|
601
|
+
const d = new Date(iso);
|
|
602
|
+
return d.toISOString().split("T")[0] ?? iso;
|
|
603
|
+
} catch {
|
|
604
|
+
return iso;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
function buildHeader(report, scanScope) {
|
|
608
|
+
const date = formatDate(report.generatedAt);
|
|
609
|
+
return [
|
|
610
|
+
"# EuConform Scan Report",
|
|
611
|
+
"",
|
|
612
|
+
"> **Disclaimer:** This is technical guidance only. It does not constitute legal advice.",
|
|
613
|
+
`> Generated by EuConform v${report.tool.version} on ${date}.`,
|
|
614
|
+
`> Scan scope: \`${scanScope}\`.`
|
|
615
|
+
].join("\n");
|
|
616
|
+
}
|
|
617
|
+
function buildProjectOverview(report) {
|
|
618
|
+
const lines = [
|
|
619
|
+
"## Project Overview",
|
|
620
|
+
"",
|
|
621
|
+
`- **Name:** ${report.target.name}`,
|
|
622
|
+
`- **Type:** ${report.target.repoType}`
|
|
623
|
+
];
|
|
624
|
+
if (report.target.detectedStack.length > 0) {
|
|
625
|
+
lines.push(`- **Stack:** ${report.target.detectedStack.join(", ")}`);
|
|
626
|
+
} else {
|
|
627
|
+
lines.push("- **Stack:** Not detected");
|
|
628
|
+
}
|
|
629
|
+
return lines.join("\n");
|
|
630
|
+
}
|
|
631
|
+
function buildAiComponents(aibom) {
|
|
632
|
+
const lines = ["## AI Components Detected", ""];
|
|
633
|
+
if (aibom.components.length === 0) {
|
|
634
|
+
lines.push("No AI components detected.");
|
|
635
|
+
return lines.join("\n");
|
|
636
|
+
}
|
|
637
|
+
lines.push("| Component | Kind | Source |");
|
|
638
|
+
lines.push("|-----------|------|--------|");
|
|
639
|
+
for (const comp of aibom.components) {
|
|
640
|
+
lines.push(`| ${comp.name} | ${bomKindLabel(comp.kind)} | ${comp.source} |`);
|
|
641
|
+
}
|
|
642
|
+
return lines.join("\n");
|
|
643
|
+
}
|
|
644
|
+
function buildComplianceSignals(report) {
|
|
645
|
+
const lines = ["## Compliance Signals", ""];
|
|
646
|
+
lines.push("| Area | Status | Confidence |");
|
|
647
|
+
lines.push("|------|--------|------------|");
|
|
648
|
+
const areas = Object.keys(COMPLIANCE_LABELS);
|
|
649
|
+
for (const area of areas) {
|
|
650
|
+
const group = report.complianceSignals[area];
|
|
651
|
+
lines.push(
|
|
652
|
+
`| ${COMPLIANCE_LABELS[area]} | ${STATUS_LABELS[group.status]} | ${group.confidence} |`
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
return lines.join("\n");
|
|
656
|
+
}
|
|
657
|
+
function buildAssessmentHints(report) {
|
|
658
|
+
const { riskIndicators, gpaiIndicators, possibleModes } = report.assessmentHints;
|
|
659
|
+
const lines = ["## Assessment Hints", ""];
|
|
660
|
+
if (gpaiIndicators.length === 0 && riskIndicators.length === 0 && possibleModes.length === 0) {
|
|
661
|
+
lines.push("No assessment hints generated (no AI detected).");
|
|
662
|
+
return lines.join("\n");
|
|
663
|
+
}
|
|
664
|
+
if (possibleModes.length > 0) {
|
|
665
|
+
lines.push(`**Possible classification modes:** ${possibleModes.join(", ")}`);
|
|
666
|
+
lines.push("");
|
|
667
|
+
}
|
|
668
|
+
if (gpaiIndicators.length > 0) {
|
|
669
|
+
lines.push("### GPAI Indicators");
|
|
670
|
+
lines.push("");
|
|
671
|
+
for (const hint of gpaiIndicators) {
|
|
672
|
+
const ref = hint.articleRef ? ` (${hint.articleRef})` : "";
|
|
673
|
+
lines.push(`- ${hint.hint}${ref}`);
|
|
674
|
+
}
|
|
675
|
+
lines.push("");
|
|
676
|
+
}
|
|
677
|
+
if (riskIndicators.length > 0) {
|
|
678
|
+
lines.push("### Risk Indicators");
|
|
679
|
+
lines.push("");
|
|
680
|
+
for (const hint of riskIndicators) {
|
|
681
|
+
const ref = hint.articleRef ? ` (${hint.articleRef})` : "";
|
|
682
|
+
lines.push(`- ${hint.hint}${ref}`);
|
|
683
|
+
}
|
|
684
|
+
lines.push("");
|
|
685
|
+
}
|
|
686
|
+
return lines.join("\n");
|
|
687
|
+
}
|
|
688
|
+
function buildOpenQuestions(report) {
|
|
689
|
+
const questions = report.assessmentHints.openQuestions;
|
|
690
|
+
const lines = ["## Open Questions", ""];
|
|
691
|
+
if (questions.length === 0) {
|
|
692
|
+
lines.push("No open questions.");
|
|
693
|
+
return lines.join("\n");
|
|
694
|
+
}
|
|
695
|
+
questions.forEach((q, i) => {
|
|
696
|
+
lines.push(`${i + 1}. ${q}`);
|
|
697
|
+
});
|
|
698
|
+
return lines.join("\n");
|
|
699
|
+
}
|
|
700
|
+
function buildRecommendedActions(report) {
|
|
701
|
+
const lines = ["## Recommended Actions", ""];
|
|
702
|
+
if (report.recommendationSummary.length === 0) {
|
|
703
|
+
lines.push("No recommended actions.");
|
|
704
|
+
return lines.join("\n");
|
|
705
|
+
}
|
|
706
|
+
for (const rec of report.recommendationSummary) {
|
|
707
|
+
lines.push(`- ${rec}`);
|
|
708
|
+
}
|
|
709
|
+
return lines.join("\n");
|
|
710
|
+
}
|
|
711
|
+
function buildFooter() {
|
|
712
|
+
return ["---", "*Full details: `euconform.report.json` -- AI BOM: `euconform.aibom.json`*"].join(
|
|
713
|
+
"\n"
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
function generateSummaryMarkdown(report, aibom, scanScope) {
|
|
717
|
+
const sections = [
|
|
718
|
+
buildHeader(report, scanScope),
|
|
719
|
+
buildProjectOverview(report),
|
|
720
|
+
buildAiComponents(aibom),
|
|
721
|
+
buildComplianceSignals(report),
|
|
722
|
+
buildAssessmentHints(report),
|
|
723
|
+
buildOpenQuestions(report),
|
|
724
|
+
buildRecommendedActions(report),
|
|
725
|
+
buildFooter()
|
|
726
|
+
];
|
|
727
|
+
return `${sections.join("\n\n")}
|
|
728
|
+
`;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// ../core/src/evidence/output.ts
|
|
732
|
+
var COMPLIANCE_AREA_MAP = {
|
|
733
|
+
disclosure: ["compliance-disclosure"],
|
|
734
|
+
biasTesting: ["compliance-bias"],
|
|
735
|
+
reportingExports: ["compliance-reporting"],
|
|
736
|
+
loggingMonitoring: ["compliance-logging"],
|
|
737
|
+
humanOversight: ["compliance-oversight"],
|
|
738
|
+
dataGovernance: ["compliance-data"],
|
|
739
|
+
incidentReporting: ["compliance-incident"]
|
|
740
|
+
};
|
|
741
|
+
var COMPLIANCE_AREA_LABELS = {
|
|
742
|
+
disclosure: "AI disclosure",
|
|
743
|
+
biasTesting: "bias testing",
|
|
744
|
+
reportingExports: "reporting and exports",
|
|
745
|
+
loggingMonitoring: "logging and monitoring",
|
|
746
|
+
humanOversight: "human oversight",
|
|
747
|
+
dataGovernance: "data governance",
|
|
748
|
+
incidentReporting: "incident reporting"
|
|
749
|
+
};
|
|
750
|
+
var AI_CATEGORIES = /* @__PURE__ */ new Set([
|
|
751
|
+
"ai-provider",
|
|
752
|
+
"ai-framework",
|
|
753
|
+
"ai-model",
|
|
754
|
+
"local-inference"
|
|
755
|
+
]);
|
|
756
|
+
var BOM_CATEGORIES = /* @__PURE__ */ new Set([
|
|
757
|
+
"ai-provider",
|
|
758
|
+
"ai-framework",
|
|
759
|
+
"ai-model",
|
|
760
|
+
"local-inference",
|
|
761
|
+
"rag",
|
|
762
|
+
"training",
|
|
763
|
+
"framework",
|
|
764
|
+
"runtime"
|
|
765
|
+
]);
|
|
766
|
+
var CATEGORY_TO_BOM_KIND = {
|
|
767
|
+
"ai-provider": "inference-provider",
|
|
768
|
+
"ai-framework": "ai-framework",
|
|
769
|
+
"ai-model": "model",
|
|
770
|
+
"local-inference": "inference-provider",
|
|
771
|
+
framework: "framework",
|
|
772
|
+
runtime: "runtime",
|
|
773
|
+
rag: "vector-store",
|
|
774
|
+
training: "dataset"
|
|
775
|
+
};
|
|
776
|
+
var SOURCE_PRIORITY = {
|
|
777
|
+
"package.json": 5,
|
|
778
|
+
config: 4,
|
|
779
|
+
code: 3,
|
|
780
|
+
"lock-file": 2,
|
|
781
|
+
docs: 1
|
|
782
|
+
};
|
|
783
|
+
var EVIDENCE_PROVENANCE_PRIORITY = {
|
|
784
|
+
runtime: 5,
|
|
785
|
+
config: 4,
|
|
786
|
+
tooling: 3,
|
|
787
|
+
"root-docs": 2,
|
|
788
|
+
docs: 1,
|
|
789
|
+
test: 0,
|
|
790
|
+
fixture: 0,
|
|
791
|
+
example: 0,
|
|
792
|
+
generated: 0
|
|
793
|
+
};
|
|
794
|
+
function hasAI(signals) {
|
|
795
|
+
return signals.some((signal) => AI_CATEGORIES.has(signal.category));
|
|
796
|
+
}
|
|
797
|
+
function highestConfidence(signals) {
|
|
798
|
+
if (signals.some((signal) => signal.confidence === "high")) return "high";
|
|
799
|
+
if (signals.some((signal) => signal.confidence === "medium")) return "medium";
|
|
800
|
+
return "low";
|
|
801
|
+
}
|
|
802
|
+
function collectEvidence(signals) {
|
|
803
|
+
return normalizeEvidence(signals.flatMap((signal) => signal.evidence));
|
|
804
|
+
}
|
|
805
|
+
function uniquePush(items, value) {
|
|
806
|
+
if (!items.includes(value)) items.push(value);
|
|
807
|
+
}
|
|
808
|
+
function toKebabCase(value) {
|
|
809
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
810
|
+
}
|
|
811
|
+
function makeBomComponentId(kind, name) {
|
|
812
|
+
const slug = toKebabCase(name);
|
|
813
|
+
return `${kind}:${slug || "component"}`;
|
|
814
|
+
}
|
|
815
|
+
function compareEvidence(left, right) {
|
|
816
|
+
const leftPriority = left.provenance ? EVIDENCE_PROVENANCE_PRIORITY[left.provenance] : 0;
|
|
817
|
+
const rightPriority = right.provenance ? EVIDENCE_PROVENANCE_PRIORITY[right.provenance] : 0;
|
|
818
|
+
if (leftPriority !== rightPriority) {
|
|
819
|
+
return rightPriority - leftPriority;
|
|
820
|
+
}
|
|
821
|
+
if (left.file !== right.file) {
|
|
822
|
+
return left.file.localeCompare(right.file);
|
|
823
|
+
}
|
|
824
|
+
return (left.line ?? 0) - (right.line ?? 0);
|
|
825
|
+
}
|
|
826
|
+
function normalizeEvidence(evidence, maxItems = 12) {
|
|
827
|
+
const unique = /* @__PURE__ */ new Map();
|
|
828
|
+
for (const item of evidence) {
|
|
829
|
+
const key = `${item.file}:${item.line ?? 0}:${item.snippet}`;
|
|
830
|
+
if (!unique.has(key)) {
|
|
831
|
+
unique.set(key, item);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return [...unique.values()].sort(compareEvidence).slice(0, maxItems);
|
|
835
|
+
}
|
|
836
|
+
var SIGNAL_NAME_MAP = [
|
|
837
|
+
["react", "React"],
|
|
838
|
+
["express", "Express"],
|
|
839
|
+
["fastify", "Fastify"],
|
|
840
|
+
["nestjs", "NestJS"],
|
|
841
|
+
["nodejs", "Node.js"],
|
|
842
|
+
["langchain", "LangChain"],
|
|
843
|
+
["llamaindex", "LlamaIndex"],
|
|
844
|
+
["transformers", "Transformers.js"],
|
|
845
|
+
["anthropic", "Anthropic"],
|
|
846
|
+
["azure", "Azure OpenAI"],
|
|
847
|
+
["ollama", "Ollama"],
|
|
848
|
+
["onnx", "ONNX Runtime"],
|
|
849
|
+
["chroma", "ChromaDB"],
|
|
850
|
+
["qdrant", "Qdrant"],
|
|
851
|
+
["pinecone", "Pinecone"],
|
|
852
|
+
["weaviate", "Weaviate"],
|
|
853
|
+
["pgvector", "pgvector"]
|
|
854
|
+
];
|
|
855
|
+
function canonicalizeSignalName(signal) {
|
|
856
|
+
const id = signal.id.toLowerCase();
|
|
857
|
+
const lowerName = signal.name.toLowerCase();
|
|
858
|
+
if (id.includes("next") || lowerName === "nextjs") return "Next.js";
|
|
859
|
+
if (id.includes("openai")) return signal.category === "ai-provider" ? "OpenAI" : "OpenAI SDK";
|
|
860
|
+
if (id.includes("google") || id.includes("gemini")) return "Google AI";
|
|
861
|
+
if (id.includes("llamacpp") || lowerName.includes("llama.cpp")) return "llama.cpp";
|
|
862
|
+
if (id.includes("turborepo") || lowerName.includes("turborepo")) return "Monorepo (Turborepo)";
|
|
863
|
+
for (const [key, canonical] of SIGNAL_NAME_MAP) {
|
|
864
|
+
if (id.includes(key)) return canonical;
|
|
865
|
+
}
|
|
866
|
+
return signal.name;
|
|
867
|
+
}
|
|
868
|
+
var LOCAL_INFERENCE_MAP = [
|
|
869
|
+
["ollama", "local-ollama"],
|
|
870
|
+
["transformers", "browser-transformers"],
|
|
871
|
+
["onnx", "local-onnx"],
|
|
872
|
+
["llama", "local-llama"]
|
|
873
|
+
];
|
|
874
|
+
var CLOUD_PROVIDER_MAP = [
|
|
875
|
+
["openai", "cloud-openai"],
|
|
876
|
+
["anthropic", "cloud-anthropic"],
|
|
877
|
+
["google", "cloud-google"],
|
|
878
|
+
["gemini", "cloud-google"],
|
|
879
|
+
["azure", "cloud-azure"]
|
|
880
|
+
];
|
|
881
|
+
function lookupInferenceMode(id, entries, prefix, name) {
|
|
882
|
+
for (const [key, mode] of entries) {
|
|
883
|
+
if (id.includes(key)) return mode;
|
|
884
|
+
}
|
|
885
|
+
const slug = toKebabCase(name);
|
|
886
|
+
return slug ? `${prefix}-${slug}` : null;
|
|
887
|
+
}
|
|
888
|
+
function canonicalizeInferenceMode(signal) {
|
|
889
|
+
const id = signal.id.toLowerCase();
|
|
890
|
+
const name = canonicalizeSignalName(signal);
|
|
891
|
+
if (signal.category === "local-inference") {
|
|
892
|
+
return lookupInferenceMode(id, LOCAL_INFERENCE_MAP, "local", name);
|
|
893
|
+
}
|
|
894
|
+
if (signal.category === "ai-provider") {
|
|
895
|
+
return lookupInferenceMode(id, CLOUD_PROVIDER_MAP, "cloud", name);
|
|
896
|
+
}
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
function deriveSource(evidence) {
|
|
900
|
+
let strongest = "code";
|
|
901
|
+
for (const item of evidence) {
|
|
902
|
+
let source = "code";
|
|
903
|
+
if (item.file.endsWith("package.json")) source = "package.json";
|
|
904
|
+
else if (item.file.endsWith("pnpm-lock.yaml") || item.file.endsWith("package-lock.json") || item.file.endsWith("yarn.lock")) {
|
|
905
|
+
source = "lock-file";
|
|
906
|
+
} else if (item.provenance === "config") {
|
|
907
|
+
source = "config";
|
|
908
|
+
} else if (item.provenance === "root-docs" || item.provenance === "docs") {
|
|
909
|
+
source = "docs";
|
|
910
|
+
}
|
|
911
|
+
if (SOURCE_PRIORITY[source] > SOURCE_PRIORITY[strongest]) {
|
|
912
|
+
strongest = source;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
return strongest;
|
|
916
|
+
}
|
|
917
|
+
function buildAiFootprint(signals) {
|
|
918
|
+
const usesAI = hasAI(signals);
|
|
919
|
+
const inferenceModes = [];
|
|
920
|
+
const providerHints = [];
|
|
921
|
+
const ragHints = [];
|
|
922
|
+
for (const signal of signals) {
|
|
923
|
+
const mode = canonicalizeInferenceMode(signal);
|
|
924
|
+
if (mode) uniquePush(inferenceModes, mode);
|
|
925
|
+
if (signal.category === "ai-provider") {
|
|
926
|
+
uniquePush(providerHints, toKebabCase(canonicalizeSignalName(signal)));
|
|
927
|
+
}
|
|
928
|
+
if (signal.category === "rag" && signal.confidence !== "low") {
|
|
929
|
+
uniquePush(ragHints, canonicalizeSignalName(signal));
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
return { usesAI, inferenceModes, providerHints, ragHints };
|
|
933
|
+
}
|
|
934
|
+
function buildComplianceSignalGroup(implementationSignals, aiDetected) {
|
|
935
|
+
if (implementationSignals.length === 0) {
|
|
936
|
+
return {
|
|
937
|
+
status: aiDetected ? "absent" : "unknown",
|
|
938
|
+
confidence: "low",
|
|
939
|
+
evidence: []
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
const confidence = highestConfidence(implementationSignals);
|
|
943
|
+
return {
|
|
944
|
+
status: confidence === "high" ? "present" : "partial",
|
|
945
|
+
confidence,
|
|
946
|
+
evidence: collectEvidence(implementationSignals)
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
function evaluateComplianceAreas(signals, aiDetected) {
|
|
950
|
+
const result = {};
|
|
951
|
+
for (const key of Object.keys(COMPLIANCE_AREA_MAP)) {
|
|
952
|
+
const { implementationSignals, supportingSignals } = evaluateSignalsForCategories(
|
|
953
|
+
signals,
|
|
954
|
+
COMPLIANCE_AREA_MAP[key]
|
|
955
|
+
);
|
|
956
|
+
result[key] = {
|
|
957
|
+
group: buildComplianceSignalGroup(implementationSignals, aiDetected),
|
|
958
|
+
implementationSignals,
|
|
959
|
+
supportingSignals
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
return result;
|
|
963
|
+
}
|
|
964
|
+
function buildAssessmentHints2(signals, complianceAreas, openQuestions) {
|
|
965
|
+
const possibleModes = [];
|
|
966
|
+
const riskIndicators = [];
|
|
967
|
+
const gpaiIndicators = [];
|
|
968
|
+
const aiDetected = hasAI(signals);
|
|
969
|
+
if (!aiDetected) {
|
|
970
|
+
return {
|
|
971
|
+
possibleModes: [],
|
|
972
|
+
riskIndicators: [],
|
|
973
|
+
gpaiIndicators: [],
|
|
974
|
+
openQuestions: openQuestions.map((question) => question.question)
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
const hasCloudProvider = signals.some((signal) => signal.category === "ai-provider");
|
|
978
|
+
const hasLocalInference = signals.some((signal) => signal.category === "local-inference");
|
|
979
|
+
if (hasCloudProvider) {
|
|
980
|
+
possibleModes.push("gpai-user");
|
|
981
|
+
const providers = Array.from(
|
|
982
|
+
new Set(
|
|
983
|
+
signals.filter((signal) => signal.category === "ai-provider").map((signal) => canonicalizeSignalName(signal))
|
|
984
|
+
)
|
|
985
|
+
);
|
|
986
|
+
gpaiIndicators.push({
|
|
987
|
+
hint: `GPAI model usage detected \u2014 cloud providers found: ${providers.join(", ")}. Provider obligations depend on your role and downstream distribution.`,
|
|
988
|
+
articleRef: "Art. 53",
|
|
989
|
+
confidence: "medium"
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
if (hasLocalInference) {
|
|
993
|
+
gpaiIndicators.push({
|
|
994
|
+
hint: "Local deployment detected \u2014 provider-side GPAI obligations are less likely unless you distribute or provide downstream access to the model.",
|
|
995
|
+
articleRef: "Art. 53",
|
|
996
|
+
confidence: "low"
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
if (complianceAreas.biasTesting.group.status !== "absent") {
|
|
1000
|
+
riskIndicators.push({
|
|
1001
|
+
hint: "Bias evaluation infrastructure detected \u2014 this can support Art. 10 evidence if it is part of your real evaluation workflow.",
|
|
1002
|
+
articleRef: "Art. 10",
|
|
1003
|
+
confidence: complianceAreas.biasTesting.group.confidence
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
if (signals.some((signal) => signal.category === "training")) {
|
|
1007
|
+
riskIndicators.push({
|
|
1008
|
+
hint: "Training or fine-tuning signals detected \u2014 data governance and provider obligations may apply depending on your role.",
|
|
1009
|
+
articleRef: "Art. 10",
|
|
1010
|
+
confidence: "high"
|
|
1011
|
+
});
|
|
1012
|
+
possibleModes.push("gpai-provider");
|
|
1013
|
+
}
|
|
1014
|
+
for (const [key, evaluation] of Object.entries(complianceAreas)) {
|
|
1015
|
+
if (evaluation.implementationSignals.length > 0 || evaluation.supportingSignals.length === 0) {
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
riskIndicators.push({
|
|
1019
|
+
hint: `Documentation references ${COMPLIANCE_AREA_LABELS[key]}, but no production implementation evidence was detected.`,
|
|
1020
|
+
confidence: "low"
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
if (!possibleModes.includes("annex-iii-unclear")) {
|
|
1024
|
+
possibleModes.push("annex-iii-unclear");
|
|
1025
|
+
}
|
|
1026
|
+
return {
|
|
1027
|
+
possibleModes,
|
|
1028
|
+
riskIndicators,
|
|
1029
|
+
gpaiIndicators,
|
|
1030
|
+
openQuestions: openQuestions.map((question) => question.question)
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
var GAP_RULES = [
|
|
1034
|
+
{
|
|
1035
|
+
id: "gap-disclosure",
|
|
1036
|
+
title: "No AI disclosure mechanism detected",
|
|
1037
|
+
description: "The EU AI Act requires that users are informed when interacting with AI systems. No user-facing disclosure implementation was found in the production codebase.",
|
|
1038
|
+
priority: "critical",
|
|
1039
|
+
area: "disclosure"
|
|
1040
|
+
},
|
|
1041
|
+
{
|
|
1042
|
+
id: "gap-logging",
|
|
1043
|
+
title: "No logging or monitoring infrastructure detected",
|
|
1044
|
+
description: "AI system outputs and decisions should be logged for traceability and audit purposes. No production logging infrastructure was detected.",
|
|
1045
|
+
priority: "high",
|
|
1046
|
+
area: "loggingMonitoring"
|
|
1047
|
+
},
|
|
1048
|
+
{
|
|
1049
|
+
id: "gap-oversight",
|
|
1050
|
+
title: "No human oversight mechanism detected",
|
|
1051
|
+
description: "High-risk AI systems require human oversight capabilities. No production review or override mechanisms were found.",
|
|
1052
|
+
priority: "high",
|
|
1053
|
+
area: "humanOversight"
|
|
1054
|
+
},
|
|
1055
|
+
{
|
|
1056
|
+
id: "gap-bias",
|
|
1057
|
+
title: "No bias testing infrastructure detected",
|
|
1058
|
+
description: "AI systems should be evaluated for bias and discrimination. No production-grade bias evaluation tooling was found.",
|
|
1059
|
+
priority: "medium",
|
|
1060
|
+
area: "biasTesting"
|
|
1061
|
+
},
|
|
1062
|
+
{
|
|
1063
|
+
id: "gap-data",
|
|
1064
|
+
title: "No data governance mechanisms detected",
|
|
1065
|
+
description: "Training and inference data should be governed with clear provenance and quality controls. No production data governance infrastructure was found.",
|
|
1066
|
+
priority: "medium",
|
|
1067
|
+
area: "dataGovernance"
|
|
1068
|
+
},
|
|
1069
|
+
{
|
|
1070
|
+
id: "gap-incident",
|
|
1071
|
+
title: "No incident reporting mechanism detected",
|
|
1072
|
+
description: "AI system incidents should be reportable and trackable. No production incident reporting infrastructure was found.",
|
|
1073
|
+
priority: "medium",
|
|
1074
|
+
area: "incidentReporting"
|
|
1075
|
+
}
|
|
1076
|
+
];
|
|
1077
|
+
function buildGaps(complianceAreas, aiDetected) {
|
|
1078
|
+
if (!aiDetected) return [];
|
|
1079
|
+
const gaps = [];
|
|
1080
|
+
for (const rule of GAP_RULES) {
|
|
1081
|
+
const evaluation = complianceAreas[rule.area];
|
|
1082
|
+
const confidence = highestConfidence(evaluation.implementationSignals);
|
|
1083
|
+
if (evaluation.implementationSignals.length === 0) {
|
|
1084
|
+
gaps.push({
|
|
1085
|
+
id: rule.id,
|
|
1086
|
+
title: rule.title,
|
|
1087
|
+
description: rule.description,
|
|
1088
|
+
priority: rule.priority,
|
|
1089
|
+
status: "missing",
|
|
1090
|
+
basis: "scanner-rule",
|
|
1091
|
+
evidence: []
|
|
1092
|
+
});
|
|
1093
|
+
continue;
|
|
1094
|
+
}
|
|
1095
|
+
if (confidence !== "high") {
|
|
1096
|
+
gaps.push({
|
|
1097
|
+
id: rule.id,
|
|
1098
|
+
title: `${rule.title.replace("No ", "Partial ")}`.replace(" detected", ""),
|
|
1099
|
+
description: `Some implementation signals were found, but confidence is not yet high. ${rule.description}`,
|
|
1100
|
+
priority: rule.priority === "critical" ? "high" : rule.priority === "high" ? "medium" : "low",
|
|
1101
|
+
status: "partial",
|
|
1102
|
+
basis: "scanner-rule",
|
|
1103
|
+
evidence: collectEvidence(evaluation.implementationSignals)
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
return gaps;
|
|
1108
|
+
}
|
|
1109
|
+
function buildRecommendationSummary(gaps, openQuestions, complianceAreas) {
|
|
1110
|
+
const recommendations = [];
|
|
1111
|
+
const priorityOrder = {
|
|
1112
|
+
critical: 0,
|
|
1113
|
+
high: 1,
|
|
1114
|
+
medium: 2,
|
|
1115
|
+
low: 3
|
|
1116
|
+
};
|
|
1117
|
+
const sortedGaps = [...gaps].sort(
|
|
1118
|
+
(left, right) => priorityOrder[left.priority] - priorityOrder[right.priority]
|
|
1119
|
+
);
|
|
1120
|
+
for (const gap of sortedGaps.slice(0, 4)) {
|
|
1121
|
+
const prefix = gap.status === "missing" ? "Implement" : "Strengthen";
|
|
1122
|
+
recommendations.push(`[${gap.priority.toUpperCase()}] ${prefix}: ${gap.title}`);
|
|
1123
|
+
}
|
|
1124
|
+
if (recommendations.length < 4) {
|
|
1125
|
+
for (const [key, evaluation] of Object.entries(complianceAreas)) {
|
|
1126
|
+
if (evaluation.implementationSignals.length > 0 || evaluation.supportingSignals.length === 0) {
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
recommendations.push(
|
|
1130
|
+
`[LOW] Convert documented ${COMPLIANCE_AREA_LABELS[key]} into production implementation evidence`
|
|
1131
|
+
);
|
|
1132
|
+
if (recommendations.length >= 4) break;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
if (openQuestions.length > 0) {
|
|
1136
|
+
recommendations.push(
|
|
1137
|
+
`Resolve ${openQuestions.length} open question(s) that require human context`
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
const finalRecommendation = "Review the generated artifacts and complete role/risk classification in the EuConform web app";
|
|
1141
|
+
return [...recommendations.slice(0, 4), finalRecommendation];
|
|
1142
|
+
}
|
|
1143
|
+
function buildDetectedStack(signals) {
|
|
1144
|
+
const stack = [];
|
|
1145
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1146
|
+
for (const signal of signals) {
|
|
1147
|
+
if (signal.category !== "framework" && signal.category !== "runtime" && signal.category !== "ai-framework") {
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
const canonicalName = canonicalizeSignalName(signal);
|
|
1151
|
+
const key = canonicalName.toLowerCase();
|
|
1152
|
+
if (seen.has(key)) continue;
|
|
1153
|
+
seen.add(key);
|
|
1154
|
+
stack.push(canonicalName);
|
|
1155
|
+
}
|
|
1156
|
+
return stack;
|
|
1157
|
+
}
|
|
1158
|
+
function hasCapability(group) {
|
|
1159
|
+
return group.status === "present" || group.status === "partial";
|
|
1160
|
+
}
|
|
1161
|
+
function resolveBomKind(signal, canonicalName) {
|
|
1162
|
+
if (signal.category === "rag") {
|
|
1163
|
+
return canonicalName.toLowerCase().includes("embedding") ? "embedding" : "vector-store";
|
|
1164
|
+
}
|
|
1165
|
+
return CATEGORY_TO_BOM_KIND[signal.category] ?? "tool";
|
|
1166
|
+
}
|
|
1167
|
+
function buildAiBom(scanResult, complianceAreas, generatedAt) {
|
|
1168
|
+
const components = [];
|
|
1169
|
+
const componentIndex = /* @__PURE__ */ new Map();
|
|
1170
|
+
for (const signal of scanResult.signals) {
|
|
1171
|
+
if (!BOM_CATEGORIES.has(signal.category)) continue;
|
|
1172
|
+
if (signal.category === "rag" && signal.confidence === "low") continue;
|
|
1173
|
+
const canonicalName = canonicalizeSignalName(signal);
|
|
1174
|
+
const kind = resolveBomKind(signal, canonicalName);
|
|
1175
|
+
const key = `${kind}:${canonicalName.toLowerCase()}`;
|
|
1176
|
+
const source = deriveSource(signal.evidence);
|
|
1177
|
+
const existingIndex = componentIndex.get(key);
|
|
1178
|
+
if (existingIndex === void 0) {
|
|
1179
|
+
componentIndex.set(key, components.length);
|
|
1180
|
+
components.push({
|
|
1181
|
+
id: makeBomComponentId(kind, canonicalName),
|
|
1182
|
+
kind,
|
|
1183
|
+
name: canonicalName,
|
|
1184
|
+
source
|
|
1185
|
+
});
|
|
1186
|
+
continue;
|
|
1187
|
+
}
|
|
1188
|
+
const existing = components[existingIndex];
|
|
1189
|
+
if (!existing) continue;
|
|
1190
|
+
if (SOURCE_PRIORITY[source] > SOURCE_PRIORITY[existing.source]) {
|
|
1191
|
+
existing.source = source;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
const reportingSignals = complianceAreas.reportingExports.implementationSignals;
|
|
1195
|
+
return {
|
|
1196
|
+
schemaVersion: "euconform.aibom.v1",
|
|
1197
|
+
generatedAt,
|
|
1198
|
+
project: {
|
|
1199
|
+
name: scanResult.repo.name,
|
|
1200
|
+
rootPath: scanResult.meta.targetPath
|
|
1201
|
+
},
|
|
1202
|
+
components: [...components].sort(
|
|
1203
|
+
(left, right) => `${left.kind}:${left.name}`.localeCompare(`${right.kind}:${right.name}`)
|
|
1204
|
+
),
|
|
1205
|
+
complianceCapabilities: {
|
|
1206
|
+
biasEvaluation: hasCapability(complianceAreas.biasTesting.group),
|
|
1207
|
+
jsonExport: reportingSignals.some((signal) => signal.id.includes("json")),
|
|
1208
|
+
pdfExport: reportingSignals.some((signal) => signal.id.includes("pdf")),
|
|
1209
|
+
loggingInfrastructure: hasCapability(complianceAreas.loggingMonitoring.group),
|
|
1210
|
+
humanReviewFlow: hasCapability(complianceAreas.humanOversight.group),
|
|
1211
|
+
incidentHandling: hasCapability(complianceAreas.incidentReporting.group)
|
|
1212
|
+
}
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
function generateScanOutput(scanResult) {
|
|
1216
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1217
|
+
const aiDetected = hasAI(scanResult.signals);
|
|
1218
|
+
const complianceAreas = evaluateComplianceAreas(scanResult.signals, aiDetected);
|
|
1219
|
+
const report = {
|
|
1220
|
+
schemaVersion: "euconform.report.v1",
|
|
1221
|
+
generatedAt,
|
|
1222
|
+
tool: {
|
|
1223
|
+
name: "euconform",
|
|
1224
|
+
version: scanResult.meta.toolVersion
|
|
1225
|
+
},
|
|
1226
|
+
target: {
|
|
1227
|
+
rootPath: scanResult.meta.targetPath,
|
|
1228
|
+
name: scanResult.repo.name,
|
|
1229
|
+
repoType: scanResult.appTypes[0] ?? "unknown",
|
|
1230
|
+
detectedStack: buildDetectedStack(scanResult.signals)
|
|
1231
|
+
},
|
|
1232
|
+
aiFootprint: buildAiFootprint(scanResult.signals),
|
|
1233
|
+
complianceSignals: {
|
|
1234
|
+
disclosure: complianceAreas.disclosure.group,
|
|
1235
|
+
biasTesting: complianceAreas.biasTesting.group,
|
|
1236
|
+
reportingExports: complianceAreas.reportingExports.group,
|
|
1237
|
+
loggingMonitoring: complianceAreas.loggingMonitoring.group,
|
|
1238
|
+
humanOversight: complianceAreas.humanOversight.group,
|
|
1239
|
+
dataGovernance: complianceAreas.dataGovernance.group,
|
|
1240
|
+
incidentReporting: complianceAreas.incidentReporting.group
|
|
1241
|
+
},
|
|
1242
|
+
assessmentHints: buildAssessmentHints2(
|
|
1243
|
+
scanResult.signals,
|
|
1244
|
+
complianceAreas,
|
|
1245
|
+
scanResult.openQuestions
|
|
1246
|
+
),
|
|
1247
|
+
gaps: buildGaps(complianceAreas, aiDetected),
|
|
1248
|
+
recommendationSummary: []
|
|
1249
|
+
};
|
|
1250
|
+
report.recommendationSummary = buildRecommendationSummary(
|
|
1251
|
+
report.gaps,
|
|
1252
|
+
scanResult.openQuestions,
|
|
1253
|
+
complianceAreas
|
|
1254
|
+
);
|
|
1255
|
+
const aibom = buildAiBom(scanResult, complianceAreas, generatedAt);
|
|
1256
|
+
const summaryMarkdown = generateSummaryMarkdown(report, aibom, scanResult.meta.scanScope);
|
|
1257
|
+
return { report, aibom, summaryMarkdown };
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// ../core/src/evidence/hash.ts
|
|
1261
|
+
import { createHash } from "crypto";
|
|
1262
|
+
function sha256Hex(content) {
|
|
1263
|
+
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// ../core/src/evidence/bundle.ts
|
|
1267
|
+
function extractSchemaVersion(content) {
|
|
1268
|
+
try {
|
|
1269
|
+
const parsed = JSON.parse(content);
|
|
1270
|
+
return typeof parsed.schemaVersion === "string" ? parsed.schemaVersion : void 0;
|
|
1271
|
+
} catch {
|
|
1272
|
+
return void 0;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
function makeArtifactRef(role, input, required) {
|
|
1276
|
+
const isJson = input.fileName.endsWith(".json");
|
|
1277
|
+
const ref = {
|
|
1278
|
+
role,
|
|
1279
|
+
fileName: input.fileName,
|
|
1280
|
+
sha256: sha256Hex(input.content),
|
|
1281
|
+
required
|
|
1282
|
+
};
|
|
1283
|
+
if (isJson) {
|
|
1284
|
+
const sv = extractSchemaVersion(input.content);
|
|
1285
|
+
if (sv) ref.schemaVersion = sv;
|
|
1286
|
+
} else {
|
|
1287
|
+
ref.mimeType = "text/markdown";
|
|
1288
|
+
}
|
|
1289
|
+
return ref;
|
|
1290
|
+
}
|
|
1291
|
+
function buildBundleManifest(opts) {
|
|
1292
|
+
const artifacts = [makeArtifactRef("report", opts.report, true)];
|
|
1293
|
+
if (opts.aibom) artifacts.push(makeArtifactRef("aibom", opts.aibom, false));
|
|
1294
|
+
if (opts.ci) artifacts.push(makeArtifactRef("ci", opts.ci, false));
|
|
1295
|
+
if (opts.summary) artifacts.push(makeArtifactRef("summary", opts.summary, false));
|
|
1296
|
+
return {
|
|
1297
|
+
schemaVersion: "euconform.bundle.v1",
|
|
1298
|
+
generatedAt: opts.generatedAt,
|
|
1299
|
+
tool: opts.tool,
|
|
1300
|
+
target: opts.target,
|
|
1301
|
+
artifacts
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// ../core/src/evidence/validate.ts
|
|
1306
|
+
function assertObject(data, label) {
|
|
1307
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
1308
|
+
throw new Error(`Invalid ${label}: expected a JSON object`);
|
|
1309
|
+
}
|
|
1310
|
+
return data;
|
|
1311
|
+
}
|
|
1312
|
+
function requireSchemaVersion(obj, expected, label) {
|
|
1313
|
+
if (obj.schemaVersion !== expected) {
|
|
1314
|
+
throw new Error(
|
|
1315
|
+
`Invalid ${label} schema version: expected "${expected}", got "${String(obj.schemaVersion)}"`
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
function requireField(obj, key, type, label, fieldName = key) {
|
|
1320
|
+
if (!(key in obj)) {
|
|
1321
|
+
throw new Error(`Invalid ${label}: missing '${fieldName}'`);
|
|
1322
|
+
}
|
|
1323
|
+
const value = obj[key];
|
|
1324
|
+
const isValid = type === "array" && Array.isArray(value) || type === "object" && value && typeof value === "object" && !Array.isArray(value) || type === "string" && typeof value === "string" || type === "boolean" && typeof value === "boolean" || type === "number" && typeof value === "number";
|
|
1325
|
+
if (!isValid) {
|
|
1326
|
+
throw new Error(`Invalid ${label}: '${fieldName}' must be a ${type}`);
|
|
1327
|
+
}
|
|
1328
|
+
return value;
|
|
1329
|
+
}
|
|
1330
|
+
function requireStringArray(obj, key, label, fieldName = key) {
|
|
1331
|
+
const value = requireField(obj, key, "array", label, fieldName);
|
|
1332
|
+
if (!value.every((item) => typeof item === "string")) {
|
|
1333
|
+
throw new Error(`Invalid ${label}: '${fieldName}' must contain strings`);
|
|
1334
|
+
}
|
|
1335
|
+
return value;
|
|
1336
|
+
}
|
|
1337
|
+
function majorVersion(schemaVersion) {
|
|
1338
|
+
const match = /\.v(\d+)$/.exec(schemaVersion);
|
|
1339
|
+
return match?.[1] ?? null;
|
|
1340
|
+
}
|
|
1341
|
+
function getSchemaMajorVersion(schemaVersion) {
|
|
1342
|
+
return majorVersion(schemaVersion);
|
|
1343
|
+
}
|
|
1344
|
+
function validateScanReport(data) {
|
|
1345
|
+
const obj = assertObject(data, "report");
|
|
1346
|
+
requireSchemaVersion(obj, "euconform.report.v1", "report");
|
|
1347
|
+
requireField(obj, "generatedAt", "string", "report");
|
|
1348
|
+
const tool = requireField(obj, "tool", "object", "report");
|
|
1349
|
+
requireField(tool, "name", "string", "report", "tool.name");
|
|
1350
|
+
requireField(tool, "version", "string", "report", "tool.version");
|
|
1351
|
+
const target = requireField(obj, "target", "object", "report");
|
|
1352
|
+
requireField(target, "rootPath", "string", "report", "target.rootPath");
|
|
1353
|
+
requireField(target, "name", "string", "report", "target.name");
|
|
1354
|
+
requireField(target, "repoType", "string", "report", "target.repoType");
|
|
1355
|
+
requireField(target, "detectedStack", "array", "report", "target.detectedStack");
|
|
1356
|
+
const aiFootprint = requireField(obj, "aiFootprint", "object", "report");
|
|
1357
|
+
requireField(aiFootprint, "usesAI", "boolean", "report", "aiFootprint.usesAI");
|
|
1358
|
+
requireStringArray(aiFootprint, "inferenceModes", "report", "aiFootprint.inferenceModes");
|
|
1359
|
+
requireStringArray(aiFootprint, "providerHints", "report", "aiFootprint.providerHints");
|
|
1360
|
+
requireStringArray(aiFootprint, "ragHints", "report", "aiFootprint.ragHints");
|
|
1361
|
+
requireField(obj, "complianceSignals", "object", "report");
|
|
1362
|
+
const hints = requireField(obj, "assessmentHints", "object", "report");
|
|
1363
|
+
requireStringArray(hints, "possibleModes", "report", "assessmentHints.possibleModes");
|
|
1364
|
+
requireField(hints, "riskIndicators", "array", "report", "assessmentHints.riskIndicators");
|
|
1365
|
+
requireField(hints, "gpaiIndicators", "array", "report", "assessmentHints.gpaiIndicators");
|
|
1366
|
+
requireStringArray(hints, "openQuestions", "report", "assessmentHints.openQuestions");
|
|
1367
|
+
requireField(obj, "gaps", "array", "report");
|
|
1368
|
+
requireStringArray(obj, "recommendationSummary", "report");
|
|
1369
|
+
return data;
|
|
1370
|
+
}
|
|
1371
|
+
function validateAiBillOfMaterials(data) {
|
|
1372
|
+
const obj = assertObject(data, "AIBOM");
|
|
1373
|
+
requireSchemaVersion(obj, "euconform.aibom.v1", "AIBOM");
|
|
1374
|
+
requireField(obj, "generatedAt", "string", "AIBOM");
|
|
1375
|
+
const project = requireField(obj, "project", "object", "AIBOM");
|
|
1376
|
+
requireField(project, "name", "string", "AIBOM", "project.name");
|
|
1377
|
+
requireField(project, "rootPath", "string", "AIBOM", "project.rootPath");
|
|
1378
|
+
requireField(obj, "components", "array", "AIBOM");
|
|
1379
|
+
requireField(obj, "complianceCapabilities", "object", "AIBOM");
|
|
1380
|
+
return data;
|
|
1381
|
+
}
|
|
1382
|
+
function validateCiReport(data) {
|
|
1383
|
+
const obj = assertObject(data, "CI report");
|
|
1384
|
+
requireSchemaVersion(obj, "euconform.ci.v1", "CI report");
|
|
1385
|
+
requireField(obj, "generatedAt", "string", "CI report");
|
|
1386
|
+
const target = requireField(obj, "target", "object", "CI report");
|
|
1387
|
+
requireField(target, "name", "string", "CI report", "target.name");
|
|
1388
|
+
requireField(target, "rootPath", "string", "CI report", "target.rootPath");
|
|
1389
|
+
const status = requireField(obj, "status", "object", "CI report");
|
|
1390
|
+
requireField(status, "failOn", "string", "CI report", "status.failOn");
|
|
1391
|
+
requireField(status, "failing", "boolean", "CI report", "status.failing");
|
|
1392
|
+
requireField(status, "openQuestions", "number", "CI report", "status.openQuestions");
|
|
1393
|
+
const gapCounts = requireField(
|
|
1394
|
+
status,
|
|
1395
|
+
"gapCounts",
|
|
1396
|
+
"object",
|
|
1397
|
+
"CI report",
|
|
1398
|
+
"status.gapCounts"
|
|
1399
|
+
);
|
|
1400
|
+
for (const key of ["critical", "high", "medium", "low"]) {
|
|
1401
|
+
requireField(gapCounts, key, "number", "CI report", `status.gapCounts.${key}`);
|
|
1402
|
+
}
|
|
1403
|
+
requireField(obj, "aiDetected", "boolean", "CI report");
|
|
1404
|
+
requireField(obj, "scanScope", "string", "CI report");
|
|
1405
|
+
requireField(obj, "artifacts", "array", "CI report");
|
|
1406
|
+
requireField(obj, "complianceOverview", "array", "CI report");
|
|
1407
|
+
requireField(obj, "topGaps", "array", "CI report");
|
|
1408
|
+
return data;
|
|
1409
|
+
}
|
|
1410
|
+
var VALID_BUNDLE_ROLES = /* @__PURE__ */ new Set(["report", "aibom", "ci", "summary"]);
|
|
1411
|
+
var SHA256_HEX = /^[a-f0-9]{64}$/;
|
|
1412
|
+
function validateScanBundle(data) {
|
|
1413
|
+
const obj = assertObject(data, "bundle");
|
|
1414
|
+
requireSchemaVersion(obj, "euconform.bundle.v1", "bundle");
|
|
1415
|
+
requireField(obj, "generatedAt", "string", "bundle");
|
|
1416
|
+
const tool = requireField(obj, "tool", "object", "bundle");
|
|
1417
|
+
requireField(tool, "name", "string", "bundle", "tool.name");
|
|
1418
|
+
requireField(tool, "version", "string", "bundle", "tool.version");
|
|
1419
|
+
const target = requireField(obj, "target", "object", "bundle");
|
|
1420
|
+
requireField(target, "name", "string", "bundle", "target.name");
|
|
1421
|
+
requireField(target, "rootPath", "string", "bundle", "target.rootPath");
|
|
1422
|
+
const artifacts = requireField(obj, "artifacts", "array", "bundle");
|
|
1423
|
+
if (artifacts.length === 0) {
|
|
1424
|
+
throw new Error("Invalid bundle: 'artifacts' must contain at least one artifact");
|
|
1425
|
+
}
|
|
1426
|
+
const seenRoles = /* @__PURE__ */ new Set();
|
|
1427
|
+
const seenFiles = /* @__PURE__ */ new Set();
|
|
1428
|
+
const bundleMajor = majorVersion("euconform.bundle.v1");
|
|
1429
|
+
let hasRequiredReport = false;
|
|
1430
|
+
for (let index = 0; index < artifacts.length; index++) {
|
|
1431
|
+
const artifact = assertObject(artifacts[index], "bundle artifact");
|
|
1432
|
+
const role = requireField(
|
|
1433
|
+
artifact,
|
|
1434
|
+
"role",
|
|
1435
|
+
"string",
|
|
1436
|
+
"bundle artifact",
|
|
1437
|
+
`artifacts[${index}].role`
|
|
1438
|
+
);
|
|
1439
|
+
const fileName = requireField(
|
|
1440
|
+
artifact,
|
|
1441
|
+
"fileName",
|
|
1442
|
+
"string",
|
|
1443
|
+
"bundle artifact",
|
|
1444
|
+
`artifacts[${index}].fileName`
|
|
1445
|
+
);
|
|
1446
|
+
const sha256 = requireField(
|
|
1447
|
+
artifact,
|
|
1448
|
+
"sha256",
|
|
1449
|
+
"string",
|
|
1450
|
+
"bundle artifact",
|
|
1451
|
+
`artifacts[${index}].sha256`
|
|
1452
|
+
);
|
|
1453
|
+
const required = requireField(
|
|
1454
|
+
artifact,
|
|
1455
|
+
"required",
|
|
1456
|
+
"boolean",
|
|
1457
|
+
"bundle artifact",
|
|
1458
|
+
`artifacts[${index}].required`
|
|
1459
|
+
);
|
|
1460
|
+
if (!VALID_BUNDLE_ROLES.has(role)) {
|
|
1461
|
+
throw new Error(`Invalid bundle artifact: unsupported role '${role}'`);
|
|
1462
|
+
}
|
|
1463
|
+
if (!SHA256_HEX.test(sha256)) {
|
|
1464
|
+
throw new Error(`Invalid bundle artifact: '${fileName}' has invalid sha256`);
|
|
1465
|
+
}
|
|
1466
|
+
if (seenRoles.has(role)) {
|
|
1467
|
+
throw new Error(`Invalid bundle artifact: duplicate role '${role}'`);
|
|
1468
|
+
}
|
|
1469
|
+
if (seenFiles.has(fileName)) {
|
|
1470
|
+
throw new Error(`Invalid bundle artifact: duplicate fileName '${fileName}'`);
|
|
1471
|
+
}
|
|
1472
|
+
seenRoles.add(role);
|
|
1473
|
+
seenFiles.add(fileName);
|
|
1474
|
+
const declaredSchemaVersion = artifact.schemaVersion;
|
|
1475
|
+
if (declaredSchemaVersion !== void 0 && typeof declaredSchemaVersion !== "string") {
|
|
1476
|
+
throw new Error(`Invalid bundle artifact: '${fileName}' has non-string schemaVersion`);
|
|
1477
|
+
}
|
|
1478
|
+
if (artifact.mimeType !== void 0 && typeof artifact.mimeType !== "string") {
|
|
1479
|
+
throw new Error(`Invalid bundle artifact: '${fileName}' has non-string mimeType`);
|
|
1480
|
+
}
|
|
1481
|
+
if (declaredSchemaVersion && bundleMajor && majorVersion(declaredSchemaVersion) !== bundleMajor) {
|
|
1482
|
+
throw new Error(
|
|
1483
|
+
`Invalid bundle artifact: '${fileName}' uses schemaVersion '${declaredSchemaVersion}' with a different major than the bundle`
|
|
1484
|
+
);
|
|
1485
|
+
}
|
|
1486
|
+
if (role === "report") {
|
|
1487
|
+
hasRequiredReport = required;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
if (!hasRequiredReport) {
|
|
1491
|
+
throw new Error("Invalid bundle: a required 'report' artifact is mandatory");
|
|
1492
|
+
}
|
|
1493
|
+
return data;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// ../core/src/scanner/aggregator.ts
|
|
1497
|
+
import { basename as basename3 } from "path";
|
|
1498
|
+
|
|
1499
|
+
// ../core/src/scanner/detectors/shared.ts
|
|
1500
|
+
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
1501
|
+
var EXTENDED_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1502
|
+
".ts",
|
|
1503
|
+
".tsx",
|
|
1504
|
+
".js",
|
|
1505
|
+
".jsx",
|
|
1506
|
+
".json",
|
|
1507
|
+
".yaml",
|
|
1508
|
+
".yml"
|
|
1509
|
+
]);
|
|
1510
|
+
var CONFIDENCE_RANK = {
|
|
1511
|
+
high: 2,
|
|
1512
|
+
medium: 1,
|
|
1513
|
+
low: 0
|
|
1514
|
+
};
|
|
1515
|
+
function higherConfidence(a, b) {
|
|
1516
|
+
return CONFIDENCE_RANK[a] >= CONFIDENCE_RANK[b] ? a : b;
|
|
1517
|
+
}
|
|
1518
|
+
function escapeRegex(str) {
|
|
1519
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
// ../core/src/scanner/detectors/ai-framework-detector.ts
|
|
1523
|
+
var FRAMEWORK_IMPORTS = [
|
|
1524
|
+
{
|
|
1525
|
+
id: "ai-framework-langchain",
|
|
1526
|
+
name: "LangChain",
|
|
1527
|
+
importPattern: /(?:from\s+['"]langchain[/'"]|require\s*\(\s*['"]langchain[/'"])/
|
|
1528
|
+
},
|
|
1529
|
+
{
|
|
1530
|
+
id: "ai-framework-langchain-core",
|
|
1531
|
+
name: "LangChain Core",
|
|
1532
|
+
importPattern: /(?:from\s+['"]@langchain\/[^'"]+['"]|require\s*\(\s*['"]@langchain\/[^'"]+['"])/
|
|
1533
|
+
},
|
|
1534
|
+
{
|
|
1535
|
+
id: "ai-framework-llamaindex",
|
|
1536
|
+
name: "LlamaIndex",
|
|
1537
|
+
importPattern: /(?:from\s+['"]llamaindex[/'"]|require\s*\(\s*['"]llamaindex[/'"])/
|
|
1538
|
+
},
|
|
1539
|
+
{
|
|
1540
|
+
id: "ai-framework-transformers",
|
|
1541
|
+
name: "Transformers.js",
|
|
1542
|
+
importPattern: /(?:from\s+['"]@xenova\/transformers['"]|require\s*\(\s*['"]@xenova\/transformers['"])/
|
|
1543
|
+
},
|
|
1544
|
+
{
|
|
1545
|
+
id: "ai-framework-vercel-ai",
|
|
1546
|
+
name: "Vercel AI SDK",
|
|
1547
|
+
// Only match Vercel AI SDK subpath imports (ai/react, ai/rsc) to avoid false positives
|
|
1548
|
+
// on generic "ai" package name. Bare "ai" import matched only with package.json cross-check.
|
|
1549
|
+
importPattern: /(?:from\s+['"]ai\/(?:react|rsc|svelte|vue|solid|core)['"']|require\s*\(\s*['"]ai\/(?:react|rsc|svelte|vue|solid|core)['"])/
|
|
1550
|
+
},
|
|
1551
|
+
{
|
|
1552
|
+
id: "ai-framework-huggingface",
|
|
1553
|
+
name: "HuggingFace",
|
|
1554
|
+
importPattern: /(?:from\s+['"]@huggingface\/[^'"]+['"]|require\s*\(\s*['"]@huggingface\/[^'"]+['"])/
|
|
1555
|
+
}
|
|
1556
|
+
];
|
|
1557
|
+
function detectAiFrameworkSignals(ctx) {
|
|
1558
|
+
const { file } = ctx;
|
|
1559
|
+
if (!SOURCE_EXTENSIONS.has(file.extension)) return [];
|
|
1560
|
+
const lines = ctx.lines;
|
|
1561
|
+
const signalMap = /* @__PURE__ */ new Map();
|
|
1562
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1563
|
+
const line = lines[i];
|
|
1564
|
+
if (!line) continue;
|
|
1565
|
+
const lineNum = i + 1;
|
|
1566
|
+
for (const framework of FRAMEWORK_IMPORTS) {
|
|
1567
|
+
if (framework.importPattern.test(line)) {
|
|
1568
|
+
const existing = signalMap.get(framework.id);
|
|
1569
|
+
const evidence = {
|
|
1570
|
+
file: file.relativePath,
|
|
1571
|
+
line: lineNum,
|
|
1572
|
+
snippet: line.trim().slice(0, 200)
|
|
1573
|
+
};
|
|
1574
|
+
if (existing) {
|
|
1575
|
+
existing.evidence.push(evidence);
|
|
1576
|
+
} else {
|
|
1577
|
+
signalMap.set(framework.id, {
|
|
1578
|
+
id: framework.id,
|
|
1579
|
+
name: framework.name,
|
|
1580
|
+
category: "ai-framework",
|
|
1581
|
+
confidence: "medium",
|
|
1582
|
+
evidence: [evidence]
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
return Array.from(signalMap.values());
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
// ../core/src/scanner/detectors/ai-provider-detector.ts
|
|
1592
|
+
var PROVIDER_IMPORTS = [
|
|
1593
|
+
{
|
|
1594
|
+
id: "ai-provider-openai",
|
|
1595
|
+
name: "OpenAI SDK",
|
|
1596
|
+
importPattern: /(?:from\s+['"]openai['"]|require\s*\(\s*['"]openai['"])/
|
|
1597
|
+
},
|
|
1598
|
+
{
|
|
1599
|
+
id: "ai-provider-anthropic",
|
|
1600
|
+
name: "Anthropic SDK",
|
|
1601
|
+
importPattern: /(?:from\s+['"]@anthropic-ai\/sdk['"]|require\s*\(\s*['"]@anthropic-ai\/sdk['"])/
|
|
1602
|
+
},
|
|
1603
|
+
{
|
|
1604
|
+
id: "ai-provider-cohere",
|
|
1605
|
+
name: "Cohere SDK",
|
|
1606
|
+
importPattern: /(?:from\s+['"]cohere-ai['"]|require\s*\(\s*['"]cohere-ai['"])/
|
|
1607
|
+
},
|
|
1608
|
+
{
|
|
1609
|
+
id: "ai-provider-google-genai",
|
|
1610
|
+
name: "Google Generative AI",
|
|
1611
|
+
importPattern: /(?:from\s+['"]@google\/generative-ai['"]|require\s*\(\s*['"]@google\/generative-ai['"])/
|
|
1612
|
+
},
|
|
1613
|
+
{
|
|
1614
|
+
id: "ai-provider-mistral",
|
|
1615
|
+
name: "Mistral AI SDK",
|
|
1616
|
+
importPattern: /(?:from\s+['"]@mistralai\/mistralai['"]|require\s*\(\s*['"]@mistralai\/mistralai['"])/
|
|
1617
|
+
}
|
|
1618
|
+
];
|
|
1619
|
+
var API_ENDPOINTS = [
|
|
1620
|
+
{
|
|
1621
|
+
id: "ai-provider-openai-api",
|
|
1622
|
+
name: "OpenAI API endpoint",
|
|
1623
|
+
pattern: /api\.openai\.com/
|
|
1624
|
+
},
|
|
1625
|
+
{
|
|
1626
|
+
id: "ai-provider-anthropic-api",
|
|
1627
|
+
name: "Anthropic API endpoint",
|
|
1628
|
+
pattern: /api\.anthropic\.com/
|
|
1629
|
+
},
|
|
1630
|
+
{
|
|
1631
|
+
id: "ai-provider-cohere-api",
|
|
1632
|
+
name: "Cohere API endpoint",
|
|
1633
|
+
pattern: /api\.cohere\.ai/
|
|
1634
|
+
}
|
|
1635
|
+
];
|
|
1636
|
+
function detectAiProviderSignals(ctx) {
|
|
1637
|
+
const { file } = ctx;
|
|
1638
|
+
if (!SOURCE_EXTENSIONS.has(file.extension)) return [];
|
|
1639
|
+
const lines = ctx.lines;
|
|
1640
|
+
const signalMap = /* @__PURE__ */ new Map();
|
|
1641
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1642
|
+
const line = lines[i];
|
|
1643
|
+
if (!line) continue;
|
|
1644
|
+
const lineNum = i + 1;
|
|
1645
|
+
for (const provider of PROVIDER_IMPORTS) {
|
|
1646
|
+
if (provider.importPattern.test(line)) {
|
|
1647
|
+
addEvidence(signalMap, provider.id, provider.name, "medium", {
|
|
1648
|
+
file: file.relativePath,
|
|
1649
|
+
line: lineNum,
|
|
1650
|
+
snippet: line.trim().slice(0, 200)
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
for (const endpoint of API_ENDPOINTS) {
|
|
1655
|
+
if (endpoint.pattern.test(line)) {
|
|
1656
|
+
addEvidence(signalMap, endpoint.id, endpoint.name, "medium", {
|
|
1657
|
+
file: file.relativePath,
|
|
1658
|
+
line: lineNum,
|
|
1659
|
+
snippet: line.trim().slice(0, 200)
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
return Array.from(signalMap.values());
|
|
1665
|
+
}
|
|
1666
|
+
function addEvidence(map, id, name, confidence, evidence) {
|
|
1667
|
+
const existing = map.get(id);
|
|
1668
|
+
if (existing) {
|
|
1669
|
+
existing.evidence.push(evidence);
|
|
1670
|
+
} else {
|
|
1671
|
+
map.set(id, {
|
|
1672
|
+
id,
|
|
1673
|
+
name,
|
|
1674
|
+
category: "ai-provider",
|
|
1675
|
+
confidence,
|
|
1676
|
+
evidence: [evidence]
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// ../core/src/scanner/detectors/compliance-detector.ts
|
|
1682
|
+
var rules = [
|
|
1683
|
+
// --- compliance-disclosure ---
|
|
1684
|
+
{
|
|
1685
|
+
id: "compliance-disclosure-ai-generated",
|
|
1686
|
+
name: "AI disclosure text",
|
|
1687
|
+
category: "compliance-disclosure",
|
|
1688
|
+
pattern: /\b(?:generated\s+by\s+ai|ai[- ]generated|powered\s+by\s+ai)\b/i,
|
|
1689
|
+
confidence: "medium"
|
|
1690
|
+
},
|
|
1691
|
+
{
|
|
1692
|
+
id: "compliance-disclosure-ai-notice",
|
|
1693
|
+
name: "AI transparency notice",
|
|
1694
|
+
category: "compliance-disclosure",
|
|
1695
|
+
pattern: /\b(?:artificial\s+intelligence|machine\s+learning\s+model|transparency\s+notice)\b/i,
|
|
1696
|
+
confidence: "medium"
|
|
1697
|
+
},
|
|
1698
|
+
// --- compliance-logging ---
|
|
1699
|
+
{
|
|
1700
|
+
id: "compliance-logging-structured",
|
|
1701
|
+
name: "Structured logging library",
|
|
1702
|
+
category: "compliance-logging",
|
|
1703
|
+
pattern: /\bfrom\s+['"](?:winston|pino|bunyan)['"]|\brequire\(\s*['"](?:winston|pino|bunyan)['"]\)/,
|
|
1704
|
+
confidence: "medium"
|
|
1705
|
+
},
|
|
1706
|
+
{
|
|
1707
|
+
id: "compliance-logging-audit",
|
|
1708
|
+
name: "Audit trail pattern",
|
|
1709
|
+
category: "compliance-logging",
|
|
1710
|
+
pattern: /\b(?:audit[._-]?log|audit[._-]?trail|log[._-]?audit)\b/i,
|
|
1711
|
+
confidence: "medium"
|
|
1712
|
+
},
|
|
1713
|
+
{
|
|
1714
|
+
id: "compliance-logging-middleware",
|
|
1715
|
+
name: "Logging middleware",
|
|
1716
|
+
category: "compliance-logging",
|
|
1717
|
+
pattern: /\b(?:logging[Mm]iddleware|requestLogger|morgan)\b/,
|
|
1718
|
+
confidence: "low"
|
|
1719
|
+
},
|
|
1720
|
+
// --- compliance-oversight ---
|
|
1721
|
+
{
|
|
1722
|
+
id: "compliance-oversight-review",
|
|
1723
|
+
name: "Human review flow",
|
|
1724
|
+
category: "compliance-oversight",
|
|
1725
|
+
pattern: /\b(?:human[_-]?review|manual[_-]?review|approval[_-]?flow|review[_-]?gate)\b/i,
|
|
1726
|
+
confidence: "medium"
|
|
1727
|
+
},
|
|
1728
|
+
{
|
|
1729
|
+
id: "compliance-oversight-approve-reject",
|
|
1730
|
+
name: "Approval/rejection mechanism",
|
|
1731
|
+
category: "compliance-oversight",
|
|
1732
|
+
pattern: /\b(?:onApprove|onReject|handleApproval|approveAction|rejectAction)\b/,
|
|
1733
|
+
confidence: "medium"
|
|
1734
|
+
},
|
|
1735
|
+
{
|
|
1736
|
+
id: "compliance-oversight-override",
|
|
1737
|
+
name: "Override mechanism",
|
|
1738
|
+
category: "compliance-oversight",
|
|
1739
|
+
pattern: /\b(?:override[_-]?ai|human[_-]?override|manual[_-]?override)\b/i,
|
|
1740
|
+
confidence: "medium"
|
|
1741
|
+
},
|
|
1742
|
+
// --- compliance-bias ---
|
|
1743
|
+
{
|
|
1744
|
+
id: "compliance-bias-fairness",
|
|
1745
|
+
name: "Fairness/bias testing",
|
|
1746
|
+
category: "compliance-bias",
|
|
1747
|
+
pattern: /\b(?:fairness[_-]?test|bias[_-]?test|disparate[_-]?impact|demographic[_-]?parity)\b/i,
|
|
1748
|
+
confidence: "medium"
|
|
1749
|
+
},
|
|
1750
|
+
{
|
|
1751
|
+
id: "compliance-bias-benchmark",
|
|
1752
|
+
name: "Bias benchmark reference",
|
|
1753
|
+
category: "compliance-bias",
|
|
1754
|
+
pattern: /\bCrowS[_-]?Pairs\b/i,
|
|
1755
|
+
confidence: "medium"
|
|
1756
|
+
},
|
|
1757
|
+
{
|
|
1758
|
+
id: "compliance-bias-euconform",
|
|
1759
|
+
name: "EuConform bias utilities",
|
|
1760
|
+
category: "compliance-bias",
|
|
1761
|
+
pattern: /\bfrom\s+['"]@euconform\/core.*bias/,
|
|
1762
|
+
confidence: "medium"
|
|
1763
|
+
},
|
|
1764
|
+
// --- compliance-data ---
|
|
1765
|
+
{
|
|
1766
|
+
id: "compliance-data-quality",
|
|
1767
|
+
name: "Data quality checks",
|
|
1768
|
+
category: "compliance-data",
|
|
1769
|
+
pattern: /\b(?:data[_-]?quality|data[_-]?validation|dataset[_-]?doc|data[_-]?lineage)\b/i,
|
|
1770
|
+
confidence: "medium"
|
|
1771
|
+
},
|
|
1772
|
+
{
|
|
1773
|
+
id: "compliance-data-pii",
|
|
1774
|
+
name: "PII handling / anonymization",
|
|
1775
|
+
category: "compliance-data",
|
|
1776
|
+
pattern: /\b(?:pii[_-]?filter|anonymi[sz]e|pseudonymi[sz]e|redact[_-]?pii)\b/i,
|
|
1777
|
+
confidence: "medium"
|
|
1778
|
+
},
|
|
1779
|
+
{
|
|
1780
|
+
id: "compliance-data-gdpr",
|
|
1781
|
+
name: "GDPR reference",
|
|
1782
|
+
category: "compliance-data",
|
|
1783
|
+
pattern: /\bGDPR\b|data[_-]?protection[_-]?regulation/i,
|
|
1784
|
+
confidence: "low"
|
|
1785
|
+
},
|
|
1786
|
+
// --- compliance-reporting ---
|
|
1787
|
+
{
|
|
1788
|
+
id: "compliance-reporting-json",
|
|
1789
|
+
name: "JSON export capability",
|
|
1790
|
+
category: "compliance-reporting",
|
|
1791
|
+
pattern: /\b(?:exportJSON|downloadJSON|writeJSONFile|saveJSONReport|exportAuditJSON|exportAnnexJSON|generateAnnexJson)\b|(?:writeFile(?:Sync)?|saveAs|download)\s*\([^)]*(?:report|audit|annex|compliance)[^)]*\.json/i,
|
|
1792
|
+
confidence: "medium"
|
|
1793
|
+
},
|
|
1794
|
+
{
|
|
1795
|
+
id: "compliance-reporting-pdf",
|
|
1796
|
+
name: "PDF export capability",
|
|
1797
|
+
category: "compliance-reporting",
|
|
1798
|
+
pattern: /\b(?:jspdf|pdf[_-]?lib|pdfmake|generatePDF|exportPDF|downloadPDF|renderPDF|generateAnnexPdf)\b|puppeteer.*\.pdf/i,
|
|
1799
|
+
confidence: "medium"
|
|
1800
|
+
},
|
|
1801
|
+
{
|
|
1802
|
+
id: "compliance-reporting-report",
|
|
1803
|
+
name: "Report generation",
|
|
1804
|
+
category: "compliance-reporting",
|
|
1805
|
+
pattern: /\b(?:generateReport|buildReport|exportReport|downloadReport|createReport|annex[_-]?iv[_-]?report|compliance[_-]?report|audit[_-]?report)\b/i,
|
|
1806
|
+
confidence: "medium"
|
|
1807
|
+
},
|
|
1808
|
+
// --- compliance-incident ---
|
|
1809
|
+
{
|
|
1810
|
+
id: "compliance-incident-sentry",
|
|
1811
|
+
name: "Sentry error reporting",
|
|
1812
|
+
category: "compliance-incident",
|
|
1813
|
+
pattern: /\bfrom\s+['"]@sentry\/|\bSentry\.init\b|\brequire\(\s*['"]@sentry\//,
|
|
1814
|
+
confidence: "medium"
|
|
1815
|
+
},
|
|
1816
|
+
{
|
|
1817
|
+
id: "compliance-incident-boundary",
|
|
1818
|
+
name: "Error boundary",
|
|
1819
|
+
category: "compliance-incident",
|
|
1820
|
+
pattern: /\bErrorBoundary\b|componentDidCatch/,
|
|
1821
|
+
confidence: "medium"
|
|
1822
|
+
},
|
|
1823
|
+
{
|
|
1824
|
+
id: "compliance-incident-response",
|
|
1825
|
+
name: "Incident response hook",
|
|
1826
|
+
category: "compliance-incident",
|
|
1827
|
+
pattern: /\b(?:incident[_-]?report|incident[_-]?response|useIncident|alerting[_-]?hook)\b/i,
|
|
1828
|
+
confidence: "medium"
|
|
1829
|
+
}
|
|
1830
|
+
];
|
|
1831
|
+
function canEvaluateRule(rule, file) {
|
|
1832
|
+
if (!IMPLEMENTATION_PROVENANCE.has(file.provenance)) return false;
|
|
1833
|
+
if (rule.category === "compliance-disclosure") return isUserFacingPath(file.relativePath);
|
|
1834
|
+
if (rule.requiresUserFacingPath) return isUserFacingPath(file.relativePath);
|
|
1835
|
+
return true;
|
|
1836
|
+
}
|
|
1837
|
+
function gatherEvidence(lines, pattern, filePath, maxItems) {
|
|
1838
|
+
const evidence = [];
|
|
1839
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1840
|
+
const line = lines[i];
|
|
1841
|
+
if (!line) continue;
|
|
1842
|
+
if (pattern.test(line)) {
|
|
1843
|
+
evidence.push({ file: filePath, line: i + 1, snippet: line.trim().slice(0, 120) });
|
|
1844
|
+
if (evidence.length >= maxItems) break;
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
return evidence;
|
|
1848
|
+
}
|
|
1849
|
+
function detectCompliance(ctx) {
|
|
1850
|
+
const { file } = ctx;
|
|
1851
|
+
if (!SOURCE_EXTENSIONS.has(file.extension)) return [];
|
|
1852
|
+
const signalMap = /* @__PURE__ */ new Map();
|
|
1853
|
+
for (const rule of rules) {
|
|
1854
|
+
if (!canEvaluateRule(rule, file)) continue;
|
|
1855
|
+
const evidence = gatherEvidence(ctx.lines, rule.pattern, file.relativePath, 3);
|
|
1856
|
+
if (evidence.length === 0) continue;
|
|
1857
|
+
const existing = signalMap.get(rule.id);
|
|
1858
|
+
if (existing) {
|
|
1859
|
+
existing.evidence.push(...evidence);
|
|
1860
|
+
} else {
|
|
1861
|
+
signalMap.set(rule.id, {
|
|
1862
|
+
id: rule.id,
|
|
1863
|
+
name: rule.name,
|
|
1864
|
+
category: rule.category,
|
|
1865
|
+
confidence: rule.confidence,
|
|
1866
|
+
evidence
|
|
1867
|
+
});
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
return Array.from(signalMap.values());
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
// ../core/src/scanner/detectors/config-detector.ts
|
|
1874
|
+
var envKeyRules = [
|
|
1875
|
+
{
|
|
1876
|
+
id: "ai-provider-openai",
|
|
1877
|
+
name: "OpenAI API key",
|
|
1878
|
+
category: "ai-provider",
|
|
1879
|
+
pattern: /^OPENAI_API_KEY\b/,
|
|
1880
|
+
confidence: "high"
|
|
1881
|
+
},
|
|
1882
|
+
{
|
|
1883
|
+
id: "ai-provider-anthropic",
|
|
1884
|
+
name: "Anthropic API key",
|
|
1885
|
+
category: "ai-provider",
|
|
1886
|
+
pattern: /^ANTHROPIC_API_KEY\b/,
|
|
1887
|
+
confidence: "high"
|
|
1888
|
+
},
|
|
1889
|
+
{
|
|
1890
|
+
id: "ai-provider-azure-openai",
|
|
1891
|
+
name: "Azure OpenAI key",
|
|
1892
|
+
category: "ai-provider",
|
|
1893
|
+
pattern: /^AZURE_OPENAI_/,
|
|
1894
|
+
confidence: "high"
|
|
1895
|
+
},
|
|
1896
|
+
{
|
|
1897
|
+
id: "ai-provider-google-ai",
|
|
1898
|
+
name: "Google AI key",
|
|
1899
|
+
category: "ai-provider",
|
|
1900
|
+
pattern: /^(?:GOOGLE_AI_API_KEY|GEMINI_API_KEY)\b/,
|
|
1901
|
+
confidence: "high"
|
|
1902
|
+
},
|
|
1903
|
+
{
|
|
1904
|
+
id: "ai-provider-cohere",
|
|
1905
|
+
name: "Cohere API key",
|
|
1906
|
+
category: "ai-provider",
|
|
1907
|
+
pattern: /^COHERE_API_KEY\b/,
|
|
1908
|
+
confidence: "high"
|
|
1909
|
+
},
|
|
1910
|
+
{
|
|
1911
|
+
id: "ai-provider-replicate",
|
|
1912
|
+
name: "Replicate API token",
|
|
1913
|
+
category: "ai-provider",
|
|
1914
|
+
pattern: /^REPLICATE_API_TOKEN\b/,
|
|
1915
|
+
confidence: "high"
|
|
1916
|
+
},
|
|
1917
|
+
{
|
|
1918
|
+
id: "ai-provider-huggingface",
|
|
1919
|
+
name: "HuggingFace token",
|
|
1920
|
+
category: "ai-provider",
|
|
1921
|
+
pattern: /^HUGGINGFACE_TOKEN\b|^HF_TOKEN\b/,
|
|
1922
|
+
confidence: "high"
|
|
1923
|
+
},
|
|
1924
|
+
{
|
|
1925
|
+
id: "local-inference-ollama",
|
|
1926
|
+
name: "Ollama host config",
|
|
1927
|
+
category: "local-inference",
|
|
1928
|
+
pattern: /^OLLAMA_HOST\b|^OLLAMA_BASE_URL\b/,
|
|
1929
|
+
confidence: "high"
|
|
1930
|
+
},
|
|
1931
|
+
{
|
|
1932
|
+
id: "rag-pinecone",
|
|
1933
|
+
name: "Pinecone API key",
|
|
1934
|
+
category: "rag",
|
|
1935
|
+
pattern: /^PINECONE_API_KEY\b/,
|
|
1936
|
+
confidence: "high"
|
|
1937
|
+
},
|
|
1938
|
+
{
|
|
1939
|
+
id: "rag-weaviate",
|
|
1940
|
+
name: "Weaviate config",
|
|
1941
|
+
category: "rag",
|
|
1942
|
+
pattern: /^WEAVIATE_/,
|
|
1943
|
+
confidence: "high"
|
|
1944
|
+
},
|
|
1945
|
+
{
|
|
1946
|
+
id: "rag-chroma",
|
|
1947
|
+
name: "ChromaDB config",
|
|
1948
|
+
category: "rag",
|
|
1949
|
+
pattern: /^CHROMA_/,
|
|
1950
|
+
confidence: "high"
|
|
1951
|
+
}
|
|
1952
|
+
];
|
|
1953
|
+
var dockerServiceRules = [
|
|
1954
|
+
{
|
|
1955
|
+
id: "local-inference-ollama-docker",
|
|
1956
|
+
name: "Ollama Docker service",
|
|
1957
|
+
category: "local-inference",
|
|
1958
|
+
pattern: /\bollama\/ollama\b|image:\s*ollama/,
|
|
1959
|
+
confidence: "high"
|
|
1960
|
+
},
|
|
1961
|
+
{
|
|
1962
|
+
id: "rag-chromadb-docker",
|
|
1963
|
+
name: "ChromaDB Docker service",
|
|
1964
|
+
category: "rag",
|
|
1965
|
+
pattern: /\bchromadb\/chroma\b|image:\s*chroma/,
|
|
1966
|
+
confidence: "high"
|
|
1967
|
+
},
|
|
1968
|
+
{
|
|
1969
|
+
id: "rag-weaviate-docker",
|
|
1970
|
+
name: "Weaviate Docker service",
|
|
1971
|
+
category: "rag",
|
|
1972
|
+
pattern: /\bsemitechnologies\/weaviate\b|image:\s*weaviate/,
|
|
1973
|
+
confidence: "high"
|
|
1974
|
+
},
|
|
1975
|
+
{
|
|
1976
|
+
id: "rag-redis-vector",
|
|
1977
|
+
name: "Redis with vector module",
|
|
1978
|
+
category: "rag",
|
|
1979
|
+
pattern: /redis\/redis-stack|redisearch|redis-stack-server/,
|
|
1980
|
+
confidence: "high"
|
|
1981
|
+
},
|
|
1982
|
+
{
|
|
1983
|
+
id: "rag-qdrant-docker",
|
|
1984
|
+
name: "Qdrant Docker service",
|
|
1985
|
+
category: "rag",
|
|
1986
|
+
pattern: /\bqdrant\/qdrant\b|image:\s*qdrant/,
|
|
1987
|
+
confidence: "high"
|
|
1988
|
+
}
|
|
1989
|
+
];
|
|
1990
|
+
var dockerfileRules = [
|
|
1991
|
+
{
|
|
1992
|
+
id: "local-inference-pip-ai",
|
|
1993
|
+
name: "AI pip packages in Dockerfile",
|
|
1994
|
+
category: "local-inference",
|
|
1995
|
+
pattern: /pip\s+install\s+.*\b(?:torch|tensorflow|transformers|langchain|llama[-_]?cpp|vllm|onnxruntime)\b/,
|
|
1996
|
+
confidence: "high"
|
|
1997
|
+
},
|
|
1998
|
+
{
|
|
1999
|
+
id: "local-inference-model-download",
|
|
2000
|
+
name: "Model download in Dockerfile",
|
|
2001
|
+
category: "local-inference",
|
|
2002
|
+
pattern: /\b(?:huggingface-cli\s+download|wget\s+.*(?:\.gguf|\.safetensors|\.bin)|COPY\s+.*(?:model|weights))\b/i,
|
|
2003
|
+
confidence: "high"
|
|
2004
|
+
}
|
|
2005
|
+
];
|
|
2006
|
+
function isEnvExample(relativePath) {
|
|
2007
|
+
const basename4 = relativePath.split("/").pop() ?? "";
|
|
2008
|
+
return /^\.env\.(?:example|sample)$/.test(basename4);
|
|
2009
|
+
}
|
|
2010
|
+
function isDockerCompose(relativePath) {
|
|
2011
|
+
const basename4 = relativePath.split("/").pop() ?? "";
|
|
2012
|
+
return /^docker-compose/.test(basename4);
|
|
2013
|
+
}
|
|
2014
|
+
function isDockerfile(relativePath) {
|
|
2015
|
+
const basename4 = relativePath.split("/").pop() ?? "";
|
|
2016
|
+
return /^Dockerfile/.test(basename4);
|
|
2017
|
+
}
|
|
2018
|
+
function detectConfig(ctx) {
|
|
2019
|
+
const { file } = ctx;
|
|
2020
|
+
const signals = [];
|
|
2021
|
+
const lines = ctx.lines;
|
|
2022
|
+
if (isEnvExample(file.relativePath)) {
|
|
2023
|
+
detectEnvKeys(file.relativePath, lines, signals);
|
|
2024
|
+
} else if (isDockerCompose(file.relativePath)) {
|
|
2025
|
+
detectDockerServices(file.relativePath, lines, signals);
|
|
2026
|
+
} else if (isDockerfile(file.relativePath)) {
|
|
2027
|
+
detectDockerfilePatterns(file.relativePath, lines, signals);
|
|
2028
|
+
}
|
|
2029
|
+
return signals;
|
|
2030
|
+
}
|
|
2031
|
+
function extractEnvVarName(rawLine) {
|
|
2032
|
+
const line = rawLine.trim();
|
|
2033
|
+
if (!line || line.startsWith("#")) return null;
|
|
2034
|
+
const eqIndex = line.indexOf("=");
|
|
2035
|
+
return eqIndex >= 0 ? line.slice(0, eqIndex).trim() : line.trim();
|
|
2036
|
+
}
|
|
2037
|
+
function detectEnvKeys(relativePath, lines, signals) {
|
|
2038
|
+
for (const rule of envKeyRules) {
|
|
2039
|
+
const evidence = [];
|
|
2040
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2041
|
+
const varName = extractEnvVarName(lines[i] ?? "");
|
|
2042
|
+
if (varName && rule.pattern.test(varName)) {
|
|
2043
|
+
evidence.push({ file: relativePath, line: i + 1, snippet: varName });
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
if (evidence.length > 0) {
|
|
2047
|
+
signals.push({
|
|
2048
|
+
id: rule.id,
|
|
2049
|
+
name: rule.name,
|
|
2050
|
+
category: rule.category,
|
|
2051
|
+
confidence: rule.confidence,
|
|
2052
|
+
evidence
|
|
2053
|
+
});
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
function detectDockerServices(relativePath, lines, signals) {
|
|
2058
|
+
for (const rule of dockerServiceRules) {
|
|
2059
|
+
const evidence = [];
|
|
2060
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2061
|
+
const line = lines[i];
|
|
2062
|
+
if (!line) continue;
|
|
2063
|
+
if (rule.pattern.test(line)) {
|
|
2064
|
+
evidence.push({
|
|
2065
|
+
file: relativePath,
|
|
2066
|
+
line: i + 1,
|
|
2067
|
+
snippet: line.trim().slice(0, 120)
|
|
2068
|
+
});
|
|
2069
|
+
if (evidence.length >= 3) break;
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
if (evidence.length > 0) {
|
|
2073
|
+
signals.push({
|
|
2074
|
+
id: rule.id,
|
|
2075
|
+
name: rule.name,
|
|
2076
|
+
category: rule.category,
|
|
2077
|
+
confidence: rule.confidence,
|
|
2078
|
+
evidence
|
|
2079
|
+
});
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
function detectDockerfilePatterns(relativePath, lines, signals) {
|
|
2084
|
+
for (const rule of dockerfileRules) {
|
|
2085
|
+
const evidence = [];
|
|
2086
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2087
|
+
const line = lines[i];
|
|
2088
|
+
if (!line) continue;
|
|
2089
|
+
if (rule.pattern.test(line)) {
|
|
2090
|
+
evidence.push({
|
|
2091
|
+
file: relativePath,
|
|
2092
|
+
line: i + 1,
|
|
2093
|
+
snippet: line.trim().slice(0, 120)
|
|
2094
|
+
});
|
|
2095
|
+
if (evidence.length >= 3) break;
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
if (evidence.length > 0) {
|
|
2099
|
+
signals.push({
|
|
2100
|
+
id: rule.id,
|
|
2101
|
+
name: rule.name,
|
|
2102
|
+
category: rule.category,
|
|
2103
|
+
confidence: rule.confidence,
|
|
2104
|
+
evidence
|
|
2105
|
+
});
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
// ../core/src/scanner/detectors/framework-detector.ts
|
|
2111
|
+
var rules2 = [
|
|
2112
|
+
// Frameworks
|
|
2113
|
+
{
|
|
2114
|
+
name: "nextjs",
|
|
2115
|
+
category: "framework",
|
|
2116
|
+
configPattern: /^next\.config\./,
|
|
2117
|
+
importPattern: /\bfrom\s+['"]next[\/'"]|\brequire\(\s*['"]next['"]\)/,
|
|
2118
|
+
configConfidence: "high",
|
|
2119
|
+
importConfidence: "medium"
|
|
2120
|
+
},
|
|
2121
|
+
{
|
|
2122
|
+
name: "react",
|
|
2123
|
+
category: "framework",
|
|
2124
|
+
importPattern: /\bfrom\s+['"]react['"]|\brequire\(\s*['"]react['"]\)/,
|
|
2125
|
+
configConfidence: "high",
|
|
2126
|
+
importConfidence: "medium"
|
|
2127
|
+
},
|
|
2128
|
+
{
|
|
2129
|
+
name: "express",
|
|
2130
|
+
category: "framework",
|
|
2131
|
+
importPattern: /\bfrom\s+['"]express['"]|\brequire\(\s*['"]express['"]\)/,
|
|
2132
|
+
configConfidence: "high",
|
|
2133
|
+
importConfidence: "medium"
|
|
2134
|
+
},
|
|
2135
|
+
{
|
|
2136
|
+
name: "fastify",
|
|
2137
|
+
category: "framework",
|
|
2138
|
+
importPattern: /\bfrom\s+['"]fastify['"]|\brequire\(\s*['"]fastify['"]\)/,
|
|
2139
|
+
configConfidence: "high",
|
|
2140
|
+
importConfidence: "medium"
|
|
2141
|
+
},
|
|
2142
|
+
{
|
|
2143
|
+
name: "nestjs",
|
|
2144
|
+
category: "framework",
|
|
2145
|
+
importPattern: /\bfrom\s+['"]@nestjs\/|\brequire\(\s*['"]@nestjs\//,
|
|
2146
|
+
configConfidence: "high",
|
|
2147
|
+
importConfidence: "medium"
|
|
2148
|
+
},
|
|
2149
|
+
{
|
|
2150
|
+
name: "hono",
|
|
2151
|
+
category: "framework",
|
|
2152
|
+
importPattern: /\bfrom\s+['"]hono['"]|\brequire\(\s*['"]hono['"]\)/,
|
|
2153
|
+
configConfidence: "high",
|
|
2154
|
+
importConfidence: "medium"
|
|
2155
|
+
},
|
|
2156
|
+
{
|
|
2157
|
+
name: "elysia",
|
|
2158
|
+
category: "framework",
|
|
2159
|
+
importPattern: /\bfrom\s+['"]elysia['"]|\brequire\(\s*['"]elysia['"]\)/,
|
|
2160
|
+
configConfidence: "high",
|
|
2161
|
+
importConfidence: "medium"
|
|
2162
|
+
},
|
|
2163
|
+
{
|
|
2164
|
+
name: "vite",
|
|
2165
|
+
category: "framework",
|
|
2166
|
+
configPattern: /^vite\.config\./,
|
|
2167
|
+
importPattern: /\bfrom\s+['"]vite['"]|\brequire\(\s*['"]vite['"]\)/,
|
|
2168
|
+
configConfidence: "high",
|
|
2169
|
+
importConfidence: "medium"
|
|
2170
|
+
},
|
|
2171
|
+
// Runtimes
|
|
2172
|
+
{
|
|
2173
|
+
name: "nodejs",
|
|
2174
|
+
category: "runtime",
|
|
2175
|
+
importPattern: /\bfrom\s+['"]node:(?:fs|http|path|crypto|stream)['"]|\bfrom\s+['"](?:fs|http|net|dgram)['"]|\brequire\(\s*['"](?:fs|http|path|crypto)['"]\)/,
|
|
2176
|
+
configConfidence: "high",
|
|
2177
|
+
importConfidence: "medium"
|
|
2178
|
+
},
|
|
2179
|
+
{
|
|
2180
|
+
name: "bun",
|
|
2181
|
+
category: "runtime",
|
|
2182
|
+
importPattern: /\bBun\.serve\b|\bBun\.file\b|\bBun\.write\b/,
|
|
2183
|
+
configConfidence: "high",
|
|
2184
|
+
importConfidence: "medium"
|
|
2185
|
+
},
|
|
2186
|
+
{
|
|
2187
|
+
name: "deno",
|
|
2188
|
+
category: "runtime",
|
|
2189
|
+
importPattern: /\bDeno\.serve\b|\bDeno\.readFile\b|\bDeno\.env\b/,
|
|
2190
|
+
configConfidence: "high",
|
|
2191
|
+
importConfidence: "medium"
|
|
2192
|
+
}
|
|
2193
|
+
];
|
|
2194
|
+
var CONFIG_NAME_PATTERN = /^(next|vite)\.config\./;
|
|
2195
|
+
function shouldProcess(file) {
|
|
2196
|
+
if (EXTENDED_EXTENSIONS.has(file.extension)) return true;
|
|
2197
|
+
const basename4 = file.relativePath.split("/").pop() ?? "";
|
|
2198
|
+
return CONFIG_NAME_PATTERN.test(basename4);
|
|
2199
|
+
}
|
|
2200
|
+
function findImportEvidence(lines, pattern, filePath) {
|
|
2201
|
+
const evidence = [];
|
|
2202
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2203
|
+
const line = lines[i];
|
|
2204
|
+
if (!line) continue;
|
|
2205
|
+
if (pattern.test(line)) {
|
|
2206
|
+
evidence.push({ file: filePath, line: i + 1, snippet: line.trim().slice(0, 120) });
|
|
2207
|
+
if (evidence.length >= 3) break;
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
return evidence;
|
|
2211
|
+
}
|
|
2212
|
+
function detectFrameworks(ctx) {
|
|
2213
|
+
const { file } = ctx;
|
|
2214
|
+
if (!shouldProcess(file)) return [];
|
|
2215
|
+
const basename4 = file.relativePath.split("/").pop() ?? "";
|
|
2216
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2217
|
+
const signals = [];
|
|
2218
|
+
for (const rule of rules2) {
|
|
2219
|
+
if (rule.configPattern?.test(basename4) && !seen.has(rule.name)) {
|
|
2220
|
+
seen.add(rule.name);
|
|
2221
|
+
signals.push({
|
|
2222
|
+
id: `${rule.category}-${rule.name}`,
|
|
2223
|
+
name: rule.name,
|
|
2224
|
+
category: rule.category,
|
|
2225
|
+
confidence: rule.configConfidence,
|
|
2226
|
+
evidence: [
|
|
2227
|
+
{ file: file.relativePath, line: 1, snippet: `Config file detected: ${basename4}` }
|
|
2228
|
+
]
|
|
2229
|
+
});
|
|
2230
|
+
}
|
|
2231
|
+
if (!rule.importPattern) continue;
|
|
2232
|
+
const evidence = findImportEvidence(ctx.lines, rule.importPattern, file.relativePath);
|
|
2233
|
+
if (evidence.length > 0 && !seen.has(rule.name)) {
|
|
2234
|
+
seen.add(rule.name);
|
|
2235
|
+
signals.push({
|
|
2236
|
+
id: `${rule.category}-${rule.name}`,
|
|
2237
|
+
name: rule.name,
|
|
2238
|
+
category: rule.category,
|
|
2239
|
+
confidence: rule.importConfidence,
|
|
2240
|
+
evidence
|
|
2241
|
+
});
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
return signals;
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
// ../core/src/scanner/detectors/local-inference-detector.ts
|
|
2248
|
+
var RULES = [
|
|
2249
|
+
// Ollama API URL
|
|
2250
|
+
{
|
|
2251
|
+
id: "local-inference-ollama-api",
|
|
2252
|
+
name: "Ollama API (localhost:11434)",
|
|
2253
|
+
pattern: /localhost:11434/,
|
|
2254
|
+
confidence: "high"
|
|
2255
|
+
},
|
|
2256
|
+
// Ollama imports
|
|
2257
|
+
{
|
|
2258
|
+
id: "local-inference-ollama-import",
|
|
2259
|
+
name: "Ollama SDK import",
|
|
2260
|
+
pattern: /(?:from\s+['"](?:ollama|@ollama\/ollama)['"]|require\s*\(\s*['"](?:ollama|@ollama\/ollama)['"])/,
|
|
2261
|
+
confidence: "medium",
|
|
2262
|
+
extensions: [".ts", ".tsx", ".js", ".jsx"]
|
|
2263
|
+
},
|
|
2264
|
+
// Transformers.js browser usage (pipeline / env)
|
|
2265
|
+
{
|
|
2266
|
+
id: "local-inference-transformers-browser",
|
|
2267
|
+
name: "Transformers.js (browser inference)",
|
|
2268
|
+
pattern: /(?:from\s+['"]@xenova\/transformers['"]|\.pipeline\s*\(|env\.allowLocalModels)/,
|
|
2269
|
+
confidence: "medium",
|
|
2270
|
+
extensions: [".ts", ".tsx", ".js", ".jsx"]
|
|
2271
|
+
},
|
|
2272
|
+
// ONNX Runtime
|
|
2273
|
+
{
|
|
2274
|
+
id: "local-inference-onnx",
|
|
2275
|
+
name: "ONNX Runtime",
|
|
2276
|
+
pattern: /(?:from\s+['"]onnxruntime[^'"]*['"]|require\s*\(\s*['"]onnxruntime[^'"]*['"]|onnxruntime)/,
|
|
2277
|
+
confidence: "medium"
|
|
2278
|
+
},
|
|
2279
|
+
// llama.cpp references
|
|
2280
|
+
{
|
|
2281
|
+
id: "local-inference-llamacpp",
|
|
2282
|
+
name: "llama.cpp",
|
|
2283
|
+
pattern: /llama\.cpp|llama-cpp|llamacpp/,
|
|
2284
|
+
confidence: "medium"
|
|
2285
|
+
},
|
|
2286
|
+
// Docker Compose ollama service
|
|
2287
|
+
{
|
|
2288
|
+
id: "local-inference-ollama-docker",
|
|
2289
|
+
name: "Ollama Docker service",
|
|
2290
|
+
pattern: /ollama\/ollama|image:\s*ollama/,
|
|
2291
|
+
confidence: "high",
|
|
2292
|
+
extensions: [".yaml", ".yml", ".json"]
|
|
2293
|
+
}
|
|
2294
|
+
];
|
|
2295
|
+
function matchRuleToLine(rule, line, lineNum, filePath, signalMap) {
|
|
2296
|
+
if (!rule.pattern.test(line)) return;
|
|
2297
|
+
const evidence = {
|
|
2298
|
+
file: filePath,
|
|
2299
|
+
line: lineNum,
|
|
2300
|
+
snippet: line.trim().slice(0, 200)
|
|
2301
|
+
};
|
|
2302
|
+
const existing = signalMap.get(rule.id);
|
|
2303
|
+
if (existing) {
|
|
2304
|
+
existing.evidence.push(evidence);
|
|
2305
|
+
if (rule.confidence === "high") existing.confidence = "high";
|
|
2306
|
+
} else {
|
|
2307
|
+
signalMap.set(rule.id, {
|
|
2308
|
+
id: rule.id,
|
|
2309
|
+
name: rule.name,
|
|
2310
|
+
category: "local-inference",
|
|
2311
|
+
confidence: rule.confidence,
|
|
2312
|
+
evidence: [evidence]
|
|
2313
|
+
});
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
function detectLocalInferenceSignals(ctx) {
|
|
2317
|
+
const { file } = ctx;
|
|
2318
|
+
if (!EXTENDED_EXTENSIONS.has(file.extension)) return [];
|
|
2319
|
+
const lines = ctx.lines;
|
|
2320
|
+
const signalMap = /* @__PURE__ */ new Map();
|
|
2321
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2322
|
+
const line = lines[i];
|
|
2323
|
+
if (!line) continue;
|
|
2324
|
+
for (const rule of RULES) {
|
|
2325
|
+
if (rule.extensions && !rule.extensions.includes(file.extension)) continue;
|
|
2326
|
+
matchRuleToLine(rule, line, i + 1, file.relativePath, signalMap);
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
return Array.from(signalMap.values());
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
// ../core/src/scanner/detectors/markdown-detector.ts
|
|
2333
|
+
var rules3 = [
|
|
2334
|
+
// --- compliance-disclosure ---
|
|
2335
|
+
{
|
|
2336
|
+
id: "compliance-disclosure-readme",
|
|
2337
|
+
name: "AI disclosure in README",
|
|
2338
|
+
category: "compliance-disclosure",
|
|
2339
|
+
pattern: /\b(?:artificial\s+intelligence|machine\s+learning|AI[- ]powered|AI[- ]generated|uses?\s+AI|deep\s+learning|neural\s+network|large\s+language\s+model|LLM)\b/i,
|
|
2340
|
+
confidence: "low"
|
|
2341
|
+
},
|
|
2342
|
+
{
|
|
2343
|
+
id: "compliance-disclosure-transparency",
|
|
2344
|
+
name: "Transparency notice",
|
|
2345
|
+
category: "compliance-disclosure",
|
|
2346
|
+
pattern: /\b(?:transparency\s+notice|AI\s+disclosure|generated\s+by\s+AI)\b/i,
|
|
2347
|
+
confidence: "medium"
|
|
2348
|
+
},
|
|
2349
|
+
// --- Model card ---
|
|
2350
|
+
{
|
|
2351
|
+
id: "compliance-disclosure-model-card",
|
|
2352
|
+
name: "Model card documentation",
|
|
2353
|
+
category: "compliance-disclosure",
|
|
2354
|
+
pattern: /^#{1,3}\s+Model\s+Card\b/i,
|
|
2355
|
+
confidence: "medium",
|
|
2356
|
+
sectionHeading: true
|
|
2357
|
+
},
|
|
2358
|
+
// --- compliance-data ---
|
|
2359
|
+
{
|
|
2360
|
+
id: "compliance-data-docs",
|
|
2361
|
+
name: "Data governance documentation",
|
|
2362
|
+
category: "compliance-data",
|
|
2363
|
+
pattern: /^#{1,3}\s+(?:Data\s+(?:Sources?|Governance|Quality|Lineage)|Training\s+Data)\b/i,
|
|
2364
|
+
confidence: "medium",
|
|
2365
|
+
sectionHeading: true
|
|
2366
|
+
},
|
|
2367
|
+
{
|
|
2368
|
+
id: "compliance-data-reference",
|
|
2369
|
+
name: "Data source references",
|
|
2370
|
+
category: "compliance-data",
|
|
2371
|
+
pattern: /\b(?:training\s+data|data\s+sources?|dataset\s+documentation)\b/i,
|
|
2372
|
+
confidence: "low"
|
|
2373
|
+
},
|
|
2374
|
+
// --- Privacy / GDPR ---
|
|
2375
|
+
{
|
|
2376
|
+
id: "compliance-data-privacy",
|
|
2377
|
+
name: "Privacy policy reference",
|
|
2378
|
+
category: "compliance-data",
|
|
2379
|
+
pattern: /\b(?:privacy\s+policy|GDPR|data\s+protection|DSGVO)\b/i,
|
|
2380
|
+
confidence: "medium"
|
|
2381
|
+
},
|
|
2382
|
+
// --- License / AI-specific ---
|
|
2383
|
+
{
|
|
2384
|
+
id: "compliance-disclosure-license",
|
|
2385
|
+
name: "AI-relevant license terms",
|
|
2386
|
+
category: "compliance-disclosure",
|
|
2387
|
+
pattern: /\b(?:model\s+license|AI\s+license|responsible\s+AI|acceptable\s+use\s+policy|RAIL\s+license)\b/i,
|
|
2388
|
+
confidence: "medium"
|
|
2389
|
+
}
|
|
2390
|
+
];
|
|
2391
|
+
var SKIP_BASENAMES = /* @__PURE__ */ new Set([
|
|
2392
|
+
"changelog.md",
|
|
2393
|
+
"changes.md",
|
|
2394
|
+
"history.md",
|
|
2395
|
+
"contributing.md",
|
|
2396
|
+
"code_of_conduct.md"
|
|
2397
|
+
]);
|
|
2398
|
+
function isRelevantMarkdown(relativePath) {
|
|
2399
|
+
const lower = relativePath.toLowerCase();
|
|
2400
|
+
const basename4 = lower.split("/").pop() ?? "";
|
|
2401
|
+
if (basename4.startsWith("readme")) return true;
|
|
2402
|
+
if (lower.includes("/docs/") || lower.startsWith("docs/")) {
|
|
2403
|
+
return !SKIP_BASENAMES.has(basename4);
|
|
2404
|
+
}
|
|
2405
|
+
if (SKIP_BASENAMES.has(basename4)) return false;
|
|
2406
|
+
return true;
|
|
2407
|
+
}
|
|
2408
|
+
function gatherMarkdownEvidence(rule, lines, filePath) {
|
|
2409
|
+
const evidence = [];
|
|
2410
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2411
|
+
const line = lines[i];
|
|
2412
|
+
if (!line) continue;
|
|
2413
|
+
if (rule.sectionHeading && !line.startsWith("#")) continue;
|
|
2414
|
+
if (rule.pattern.test(line)) {
|
|
2415
|
+
evidence.push({ file: filePath, line: i + 1, snippet: line.trim().slice(0, 120) });
|
|
2416
|
+
if (evidence.length >= 5) break;
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
return evidence;
|
|
2420
|
+
}
|
|
2421
|
+
function isSkippableGenericContent(basename4, evidence) {
|
|
2422
|
+
if (!SKIP_BASENAMES.has(basename4)) return false;
|
|
2423
|
+
return !evidence.some((e) => /\bAI\b|\bML\b|\bmodel\b|\binference\b/i.test(e.snippet));
|
|
2424
|
+
}
|
|
2425
|
+
function detectMarkdown(ctx) {
|
|
2426
|
+
const { file } = ctx;
|
|
2427
|
+
if (file.extension !== ".md") return [];
|
|
2428
|
+
if (!isRelevantMarkdown(file.relativePath)) return [];
|
|
2429
|
+
const signalMap = /* @__PURE__ */ new Map();
|
|
2430
|
+
const basename4 = file.relativePath.toLowerCase().split("/").pop() ?? "";
|
|
2431
|
+
for (const rule of rules3) {
|
|
2432
|
+
const evidence = gatherMarkdownEvidence(rule, ctx.lines, file.relativePath);
|
|
2433
|
+
if (evidence.length === 0) continue;
|
|
2434
|
+
if (isSkippableGenericContent(basename4, evidence)) continue;
|
|
2435
|
+
const existing = signalMap.get(rule.id);
|
|
2436
|
+
if (existing) {
|
|
2437
|
+
existing.evidence.push(...evidence);
|
|
2438
|
+
} else {
|
|
2439
|
+
signalMap.set(rule.id, {
|
|
2440
|
+
id: rule.id,
|
|
2441
|
+
name: rule.name,
|
|
2442
|
+
category: rule.category,
|
|
2443
|
+
confidence: rule.confidence,
|
|
2444
|
+
evidence
|
|
2445
|
+
});
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
return Array.from(signalMap.values());
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
// ../core/src/scanner/detectors/package-json-detector.ts
|
|
2452
|
+
import { basename } from "path";
|
|
2453
|
+
var AI_PACKAGES = {
|
|
2454
|
+
// Providers
|
|
2455
|
+
openai: { category: "ai-provider", label: "OpenAI SDK" },
|
|
2456
|
+
"@anthropic-ai/sdk": { category: "ai-provider", label: "Anthropic SDK" },
|
|
2457
|
+
"cohere-ai": { category: "ai-provider", label: "Cohere SDK" },
|
|
2458
|
+
"@google/generative-ai": { category: "ai-provider", label: "Google Generative AI" },
|
|
2459
|
+
"@mistralai/mistralai": { category: "ai-provider", label: "Mistral AI SDK" },
|
|
2460
|
+
// Frameworks
|
|
2461
|
+
langchain: { category: "ai-framework", label: "LangChain" },
|
|
2462
|
+
"@langchain/core": { category: "ai-framework", label: "LangChain Core" },
|
|
2463
|
+
llamaindex: { category: "ai-framework", label: "LlamaIndex" },
|
|
2464
|
+
"@xenova/transformers": { category: "ai-framework", label: "Transformers.js" },
|
|
2465
|
+
ai: { category: "ai-framework", label: "Vercel AI SDK" },
|
|
2466
|
+
"@huggingface/inference": { category: "ai-framework", label: "HuggingFace Inference" },
|
|
2467
|
+
// Local inference
|
|
2468
|
+
ollama: { category: "local-inference", label: "Ollama" },
|
|
2469
|
+
"@ollama/ollama": { category: "local-inference", label: "Ollama SDK" },
|
|
2470
|
+
// RAG / Vector
|
|
2471
|
+
chromadb: { category: "rag", label: "ChromaDB" },
|
|
2472
|
+
"@pinecone-database/pinecone": { category: "rag", label: "Pinecone" },
|
|
2473
|
+
"weaviate-client": { category: "rag", label: "Weaviate" },
|
|
2474
|
+
"@qdrant/js-client-rest": { category: "rag", label: "Qdrant" },
|
|
2475
|
+
pgvector: { category: "rag", label: "pgvector" },
|
|
2476
|
+
// Bias / Eval
|
|
2477
|
+
"@euconform/core": { category: "compliance-bias", label: "EuConform Core" }
|
|
2478
|
+
};
|
|
2479
|
+
var FRAMEWORK_PACKAGES = {
|
|
2480
|
+
next: { category: "framework", label: "Next.js" },
|
|
2481
|
+
react: { category: "framework", label: "React" },
|
|
2482
|
+
express: { category: "framework", label: "Express" },
|
|
2483
|
+
fastify: { category: "framework", label: "Fastify" },
|
|
2484
|
+
"@nestjs/core": { category: "framework", label: "NestJS" },
|
|
2485
|
+
hono: { category: "framework", label: "Hono" },
|
|
2486
|
+
elysia: { category: "framework", label: "Elysia" }
|
|
2487
|
+
};
|
|
2488
|
+
var ALL_PACKAGES = {
|
|
2489
|
+
...AI_PACKAGES,
|
|
2490
|
+
...FRAMEWORK_PACKAGES
|
|
2491
|
+
};
|
|
2492
|
+
var PACKAGE_JSON_NAMES = /* @__PURE__ */ new Set([
|
|
2493
|
+
"package.json",
|
|
2494
|
+
"pnpm-lock.yaml",
|
|
2495
|
+
"package-lock.json",
|
|
2496
|
+
"yarn.lock"
|
|
2497
|
+
]);
|
|
2498
|
+
function isPackageJsonFile(relativePath) {
|
|
2499
|
+
return PACKAGE_JSON_NAMES.has(basename(relativePath));
|
|
2500
|
+
}
|
|
2501
|
+
function detectPackageJsonSignals(ctx) {
|
|
2502
|
+
const { file } = ctx;
|
|
2503
|
+
if (!isPackageJsonFile(file.relativePath)) return [];
|
|
2504
|
+
const fileName = basename(file.relativePath);
|
|
2505
|
+
if (fileName === "package.json") {
|
|
2506
|
+
return detectFromPackageJson(file.relativePath, file.content);
|
|
2507
|
+
}
|
|
2508
|
+
return detectFromLockFile(file.relativePath, file.content);
|
|
2509
|
+
}
|
|
2510
|
+
function detectFromPackageJson(filePath, content) {
|
|
2511
|
+
let parsed;
|
|
2512
|
+
try {
|
|
2513
|
+
parsed = JSON.parse(content);
|
|
2514
|
+
} catch {
|
|
2515
|
+
return [];
|
|
2516
|
+
}
|
|
2517
|
+
const signals = [];
|
|
2518
|
+
const deps = {
|
|
2519
|
+
...parsed.dependencies,
|
|
2520
|
+
...parsed.devDependencies
|
|
2521
|
+
};
|
|
2522
|
+
for (const [pkg, info] of Object.entries(ALL_PACKAGES)) {
|
|
2523
|
+
if (pkg in deps) {
|
|
2524
|
+
const version = deps[pkg] ?? "unknown";
|
|
2525
|
+
signals.push({
|
|
2526
|
+
id: `${info.category}-${pkg.replace(/[@/]/g, "").replace(/\s/g, "-")}`,
|
|
2527
|
+
name: info.label,
|
|
2528
|
+
category: info.category,
|
|
2529
|
+
confidence: "high",
|
|
2530
|
+
evidence: [
|
|
2531
|
+
{
|
|
2532
|
+
file: filePath,
|
|
2533
|
+
snippet: `"${pkg}": "${version}"`
|
|
2534
|
+
}
|
|
2535
|
+
]
|
|
2536
|
+
});
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
if (parsed.workspaces) {
|
|
2540
|
+
signals.push({
|
|
2541
|
+
id: "runtime-monorepo-workspaces",
|
|
2542
|
+
name: "Monorepo (workspaces)",
|
|
2543
|
+
category: "runtime",
|
|
2544
|
+
confidence: "high",
|
|
2545
|
+
evidence: [
|
|
2546
|
+
{
|
|
2547
|
+
file: filePath,
|
|
2548
|
+
snippet: `"workspaces": ${JSON.stringify(parsed.workspaces).slice(0, 120)}`
|
|
2549
|
+
}
|
|
2550
|
+
]
|
|
2551
|
+
});
|
|
2552
|
+
}
|
|
2553
|
+
const devDeps = parsed.devDependencies;
|
|
2554
|
+
if (devDeps && "turbo" in devDeps) {
|
|
2555
|
+
signals.push({
|
|
2556
|
+
id: "runtime-monorepo-turbo",
|
|
2557
|
+
name: "Monorepo (Turborepo)",
|
|
2558
|
+
category: "runtime",
|
|
2559
|
+
confidence: "high",
|
|
2560
|
+
evidence: [
|
|
2561
|
+
{
|
|
2562
|
+
file: filePath,
|
|
2563
|
+
snippet: `"turbo": "${devDeps.turbo}"`
|
|
2564
|
+
}
|
|
2565
|
+
]
|
|
2566
|
+
});
|
|
2567
|
+
}
|
|
2568
|
+
return signals;
|
|
2569
|
+
}
|
|
2570
|
+
var LOCK_FILE_PATTERNS = new Map(
|
|
2571
|
+
Object.keys(AI_PACKAGES).map((pkg) => [pkg, new RegExp(`["'/]${escapeRegex(pkg)}["'/@:]`)])
|
|
2572
|
+
);
|
|
2573
|
+
function detectFromLockFile(filePath, content) {
|
|
2574
|
+
const signals = [];
|
|
2575
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2576
|
+
for (const [pkg, info] of Object.entries(AI_PACKAGES)) {
|
|
2577
|
+
if (seen.has(pkg)) continue;
|
|
2578
|
+
const pattern = LOCK_FILE_PATTERNS.get(pkg);
|
|
2579
|
+
if (!pattern) continue;
|
|
2580
|
+
if (pattern.test(content)) {
|
|
2581
|
+
seen.add(pkg);
|
|
2582
|
+
signals.push({
|
|
2583
|
+
id: `${info.category}-${pkg.replace(/[@/]/g, "").replace(/\s/g, "-")}`,
|
|
2584
|
+
name: info.label,
|
|
2585
|
+
category: info.category,
|
|
2586
|
+
confidence: "medium",
|
|
2587
|
+
evidence: [
|
|
2588
|
+
{
|
|
2589
|
+
file: filePath,
|
|
2590
|
+
snippet: `Found "${pkg}" in lock file`
|
|
2591
|
+
}
|
|
2592
|
+
]
|
|
2593
|
+
});
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
return signals;
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
// ../core/src/scanner/detectors/rag-detector.ts
|
|
2600
|
+
var IMPORT_RULES = [
|
|
2601
|
+
{
|
|
2602
|
+
id: "rag-chromadb",
|
|
2603
|
+
name: "ChromaDB",
|
|
2604
|
+
pattern: /(?:from\s+['"]chromadb['"]|require\s*\(\s*['"]chromadb['"])/,
|
|
2605
|
+
confidence: "high"
|
|
2606
|
+
},
|
|
2607
|
+
{
|
|
2608
|
+
id: "rag-pinecone",
|
|
2609
|
+
name: "Pinecone",
|
|
2610
|
+
pattern: /(?:from\s+['"]@pinecone-database\/pinecone['"]|require\s*\(\s*['"]@pinecone-database\/pinecone['"])/,
|
|
2611
|
+
confidence: "high"
|
|
2612
|
+
},
|
|
2613
|
+
{
|
|
2614
|
+
id: "rag-weaviate",
|
|
2615
|
+
name: "Weaviate",
|
|
2616
|
+
pattern: /(?:from\s+['"]weaviate-client['"]|require\s*\(\s*['"]weaviate-client['"])/,
|
|
2617
|
+
confidence: "high"
|
|
2618
|
+
},
|
|
2619
|
+
{
|
|
2620
|
+
id: "rag-qdrant",
|
|
2621
|
+
name: "Qdrant",
|
|
2622
|
+
pattern: /(?:from\s+['"]@qdrant\/js-client-rest['"]|require\s*\(\s*['"]@qdrant\/js-client-rest['"])/,
|
|
2623
|
+
confidence: "high"
|
|
2624
|
+
},
|
|
2625
|
+
{
|
|
2626
|
+
id: "rag-pgvector",
|
|
2627
|
+
name: "pgvector",
|
|
2628
|
+
pattern: /(?:from\s+['"]pgvector['"]|require\s*\(\s*['"]pgvector['"])/,
|
|
2629
|
+
confidence: "high"
|
|
2630
|
+
},
|
|
2631
|
+
{
|
|
2632
|
+
id: "rag-faiss",
|
|
2633
|
+
name: "FAISS",
|
|
2634
|
+
pattern: /(?:from\s+['"]faiss-node['"]|require\s*\(\s*['"]faiss-node['"])/,
|
|
2635
|
+
confidence: "high"
|
|
2636
|
+
}
|
|
2637
|
+
];
|
|
2638
|
+
var USAGE_RULES = [
|
|
2639
|
+
{
|
|
2640
|
+
id: "rag-embedding-call",
|
|
2641
|
+
name: "Embedding function call",
|
|
2642
|
+
pattern: /\.(?:createEmbedding|embedDocuments|embedQuery|embed)\s*\(/,
|
|
2643
|
+
confidence: "medium"
|
|
2644
|
+
},
|
|
2645
|
+
{
|
|
2646
|
+
id: "rag-retriever-pattern",
|
|
2647
|
+
name: "Retriever pattern",
|
|
2648
|
+
pattern: /\.(?:asRetriever|getRelevantDocuments|similaritySearch|similaritySearchWithScore)\s*\(/,
|
|
2649
|
+
confidence: "medium"
|
|
2650
|
+
},
|
|
2651
|
+
{
|
|
2652
|
+
id: "rag-vectorstore-usage",
|
|
2653
|
+
name: "Vector store usage",
|
|
2654
|
+
pattern: /(?:VectorStore|vectorStore|fromDocuments|fromTexts|addDocuments)\s*\(/,
|
|
2655
|
+
confidence: "medium"
|
|
2656
|
+
}
|
|
2657
|
+
];
|
|
2658
|
+
var KEYWORD_RULES = [
|
|
2659
|
+
{
|
|
2660
|
+
id: "rag-keyword-embedding",
|
|
2661
|
+
name: "Embedding keyword reference",
|
|
2662
|
+
pattern: /\b(?:text[_-]?embedding|embedding[_-]?(?:model|vector|dimension|size)|openai.*embedding|embedding[_-]?api)\b/i,
|
|
2663
|
+
confidence: "low"
|
|
2664
|
+
},
|
|
2665
|
+
{
|
|
2666
|
+
id: "rag-keyword-vector",
|
|
2667
|
+
name: "Vector keyword reference",
|
|
2668
|
+
pattern: /\b(?:vector(?:Store|DB|Database|Index))\b/,
|
|
2669
|
+
confidence: "low"
|
|
2670
|
+
},
|
|
2671
|
+
{
|
|
2672
|
+
id: "rag-keyword-retriever",
|
|
2673
|
+
name: "Retriever keyword reference",
|
|
2674
|
+
pattern: /\bretriever\b/i,
|
|
2675
|
+
confidence: "low"
|
|
2676
|
+
},
|
|
2677
|
+
{
|
|
2678
|
+
id: "rag-keyword-similarity",
|
|
2679
|
+
name: "Similarity search keyword",
|
|
2680
|
+
pattern: /\bsimilarity_search\b/,
|
|
2681
|
+
confidence: "low"
|
|
2682
|
+
}
|
|
2683
|
+
];
|
|
2684
|
+
var ALL_RULES = [...IMPORT_RULES, ...USAGE_RULES, ...KEYWORD_RULES];
|
|
2685
|
+
function detectRagSignals(ctx) {
|
|
2686
|
+
const { file } = ctx;
|
|
2687
|
+
if (!SOURCE_EXTENSIONS.has(file.extension)) return [];
|
|
2688
|
+
const lines = ctx.lines;
|
|
2689
|
+
const signalMap = /* @__PURE__ */ new Map();
|
|
2690
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2691
|
+
const line = lines[i];
|
|
2692
|
+
if (!line) continue;
|
|
2693
|
+
const lineNum = i + 1;
|
|
2694
|
+
for (const rule of ALL_RULES) {
|
|
2695
|
+
if (rule.pattern.test(line)) {
|
|
2696
|
+
const evidence = {
|
|
2697
|
+
file: file.relativePath,
|
|
2698
|
+
line: lineNum,
|
|
2699
|
+
snippet: line.trim().slice(0, 200)
|
|
2700
|
+
};
|
|
2701
|
+
const existing = signalMap.get(rule.id);
|
|
2702
|
+
if (existing) {
|
|
2703
|
+
existing.evidence.push(evidence);
|
|
2704
|
+
existing.confidence = higherConfidence(existing.confidence, rule.confidence);
|
|
2705
|
+
} else {
|
|
2706
|
+
signalMap.set(rule.id, {
|
|
2707
|
+
id: rule.id,
|
|
2708
|
+
name: rule.name,
|
|
2709
|
+
category: "rag",
|
|
2710
|
+
confidence: rule.confidence,
|
|
2711
|
+
evidence: [evidence]
|
|
2712
|
+
});
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
return Array.from(signalMap.values());
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
// ../core/src/scanner/detectors/index.ts
|
|
2721
|
+
var ALL_DETECTORS = [
|
|
2722
|
+
detectPackageJsonSignals,
|
|
2723
|
+
detectAiProviderSignals,
|
|
2724
|
+
detectAiFrameworkSignals,
|
|
2725
|
+
detectLocalInferenceSignals,
|
|
2726
|
+
detectRagSignals,
|
|
2727
|
+
detectFrameworks,
|
|
2728
|
+
detectCompliance,
|
|
2729
|
+
detectConfig,
|
|
2730
|
+
detectMarkdown
|
|
2731
|
+
];
|
|
2732
|
+
function runAllDetectors(ctx) {
|
|
2733
|
+
const signals = [];
|
|
2734
|
+
for (const detector of ALL_DETECTORS) {
|
|
2735
|
+
try {
|
|
2736
|
+
signals.push(...detector(ctx));
|
|
2737
|
+
} catch {
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
return signals;
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
// ../core/src/scanner/discovery.ts
|
|
2744
|
+
var import_ignore = __toESM(require_ignore(), 1);
|
|
2745
|
+
import { readFile, readdir, stat } from "fs/promises";
|
|
2746
|
+
import { basename as basename2, extname, join, resolve } from "path";
|
|
2747
|
+
var DEFAULT_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".json", ".md", ".yaml", ".yml"];
|
|
2748
|
+
var NAMED_FILE_PATTERNS = [
|
|
2749
|
+
/^package\.json$/,
|
|
2750
|
+
/^pnpm-lock\.yaml$/,
|
|
2751
|
+
/^package-lock\.json$/,
|
|
2752
|
+
/^yarn\.lock$/,
|
|
2753
|
+
/^\.env\.example$/,
|
|
2754
|
+
/^\.env\.sample$/,
|
|
2755
|
+
/^next\.config\./,
|
|
2756
|
+
/^vite\.config\./,
|
|
2757
|
+
/^docker-compose/,
|
|
2758
|
+
/^Dockerfile/,
|
|
2759
|
+
/^tsconfig.*\.json$/
|
|
2760
|
+
];
|
|
2761
|
+
var ALWAYS_SKIP_DIRS = [
|
|
2762
|
+
"node_modules",
|
|
2763
|
+
".git",
|
|
2764
|
+
"dist",
|
|
2765
|
+
".next",
|
|
2766
|
+
"build",
|
|
2767
|
+
"coverage",
|
|
2768
|
+
".turbo",
|
|
2769
|
+
".cache",
|
|
2770
|
+
".euconform"
|
|
2771
|
+
];
|
|
2772
|
+
var DEFAULT_MAX_FILE_SIZE = 1048576;
|
|
2773
|
+
var DEFAULT_MAX_FILES = 1e4;
|
|
2774
|
+
var DEFAULT_SCOPE = "production";
|
|
2775
|
+
var PRODUCTION_SKIP_PROVENANCE = /* @__PURE__ */ new Set([
|
|
2776
|
+
"test",
|
|
2777
|
+
"fixture",
|
|
2778
|
+
"example",
|
|
2779
|
+
"generated"
|
|
2780
|
+
]);
|
|
2781
|
+
var TEST_SEGMENTS = /* @__PURE__ */ new Set(["test", "tests", "__test__", "__tests__", "spec", "__spec__"]);
|
|
2782
|
+
var FIXTURE_SEGMENTS = /* @__PURE__ */ new Set([
|
|
2783
|
+
"fixture",
|
|
2784
|
+
"fixtures",
|
|
2785
|
+
"__fixture__",
|
|
2786
|
+
"__fixtures__",
|
|
2787
|
+
"mocks",
|
|
2788
|
+
"__mocks__",
|
|
2789
|
+
"mock-data"
|
|
2790
|
+
]);
|
|
2791
|
+
var EXAMPLE_SEGMENTS = /* @__PURE__ */ new Set(["example", "examples", "sample", "samples", "demo", "demos"]);
|
|
2792
|
+
var GENERATED_SEGMENTS = /* @__PURE__ */ new Set(["generated", "__generated__", ".generated", "gen"]);
|
|
2793
|
+
var TOOLING_SEGMENTS = /* @__PURE__ */ new Set([".github", "scripts", "tools", "tooling", "bin"]);
|
|
2794
|
+
var TEST_FILE_PATTERN = /(?:^|\.)(?:test|spec)\.[^.]+$/i;
|
|
2795
|
+
var GENERATED_FILE_PATTERN = /(?:^|\.)(?:generated|min)\.[^.]+$/i;
|
|
2796
|
+
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
2797
|
+
".png",
|
|
2798
|
+
".jpg",
|
|
2799
|
+
".jpeg",
|
|
2800
|
+
".gif",
|
|
2801
|
+
".ico",
|
|
2802
|
+
".svg",
|
|
2803
|
+
".woff",
|
|
2804
|
+
".woff2",
|
|
2805
|
+
".ttf",
|
|
2806
|
+
".eot",
|
|
2807
|
+
".zip",
|
|
2808
|
+
".tar",
|
|
2809
|
+
".gz",
|
|
2810
|
+
".br",
|
|
2811
|
+
".pdf",
|
|
2812
|
+
".doc",
|
|
2813
|
+
".docx",
|
|
2814
|
+
".mp3",
|
|
2815
|
+
".mp4",
|
|
2816
|
+
".webm",
|
|
2817
|
+
".ogg",
|
|
2818
|
+
".wasm",
|
|
2819
|
+
".node",
|
|
2820
|
+
".dylib",
|
|
2821
|
+
".so",
|
|
2822
|
+
".lock"
|
|
2823
|
+
// bun.lockb is binary
|
|
2824
|
+
]);
|
|
2825
|
+
function isBinaryExtension(ext) {
|
|
2826
|
+
return BINARY_EXTENSIONS.has(ext);
|
|
2827
|
+
}
|
|
2828
|
+
function isNamedFile(fileName) {
|
|
2829
|
+
return NAMED_FILE_PATTERNS.some((pattern) => pattern.test(fileName));
|
|
2830
|
+
}
|
|
2831
|
+
function shouldIncludeFile(fileName, ext, extensions) {
|
|
2832
|
+
if (isNamedFile(fileName)) return true;
|
|
2833
|
+
if (isBinaryExtension(ext)) return false;
|
|
2834
|
+
return extensions.includes(ext);
|
|
2835
|
+
}
|
|
2836
|
+
function classifyFileProvenance(relativePath) {
|
|
2837
|
+
const normalized = relativePath.replace(/\\/g, "/");
|
|
2838
|
+
const rawSegments = normalized.split("/").filter(Boolean);
|
|
2839
|
+
const segments = rawSegments.map((segment) => segment.toLowerCase());
|
|
2840
|
+
const rawFileName = rawSegments[rawSegments.length - 1] ?? "";
|
|
2841
|
+
const fileName = segments[segments.length - 1] ?? "";
|
|
2842
|
+
const ext = extname(fileName);
|
|
2843
|
+
const isRootFile = segments.length === 1;
|
|
2844
|
+
if (segments.some((segment) => FIXTURE_SEGMENTS.has(segment))) return "fixture";
|
|
2845
|
+
if (segments.some((segment) => TEST_SEGMENTS.has(segment)) || TEST_FILE_PATTERN.test(fileName)) {
|
|
2846
|
+
return "test";
|
|
2847
|
+
}
|
|
2848
|
+
if (segments.some((segment) => EXAMPLE_SEGMENTS.has(segment))) return "example";
|
|
2849
|
+
if (segments.some((segment) => GENERATED_SEGMENTS.has(segment)) || GENERATED_FILE_PATTERN.test(fileName)) {
|
|
2850
|
+
return "generated";
|
|
2851
|
+
}
|
|
2852
|
+
if (segments.some((segment) => TOOLING_SEGMENTS.has(segment))) return "tooling";
|
|
2853
|
+
if (fileName.startsWith("readme") && isRootFile) return "root-docs";
|
|
2854
|
+
if (ext === ".md") return "docs";
|
|
2855
|
+
if (isNamedFile(rawFileName) || ext === ".yaml" || ext === ".yml" || fileName.startsWith(".env") || /\.config\.[^.]+$/i.test(rawFileName)) {
|
|
2856
|
+
return "config";
|
|
2857
|
+
}
|
|
2858
|
+
return "runtime";
|
|
2859
|
+
}
|
|
2860
|
+
async function loadGitignore(targetPath) {
|
|
2861
|
+
const ig = (0, import_ignore.default)();
|
|
2862
|
+
try {
|
|
2863
|
+
const content = await readFile(join(targetPath, ".gitignore"), "utf-8");
|
|
2864
|
+
ig.add(content);
|
|
2865
|
+
} catch {
|
|
2866
|
+
}
|
|
2867
|
+
ig.add(ALWAYS_SKIP_DIRS.map((d) => `${d}/`));
|
|
2868
|
+
return ig;
|
|
2869
|
+
}
|
|
2870
|
+
function shouldProcessEntry(entry, targetPath, filterOpts) {
|
|
2871
|
+
const absolutePath = join(targetPath, entry);
|
|
2872
|
+
const fileName = basename2(entry);
|
|
2873
|
+
const ext = extname(fileName).toLowerCase();
|
|
2874
|
+
const provenance = classifyFileProvenance(entry);
|
|
2875
|
+
if (filterOpts.scope === "production" && PRODUCTION_SKIP_PROVENANCE.has(provenance)) {
|
|
2876
|
+
return "skip";
|
|
2877
|
+
}
|
|
2878
|
+
if (!shouldIncludeFile(fileName, ext, filterOpts.extensions)) return "ignore";
|
|
2879
|
+
return { absolutePath, ext, provenance };
|
|
2880
|
+
}
|
|
2881
|
+
async function tryReadFile(absolutePath, entry, ext, provenance, maxSize) {
|
|
2882
|
+
try {
|
|
2883
|
+
const fileStat = await stat(absolutePath);
|
|
2884
|
+
if (!fileStat.isFile() || fileStat.size === 0) return "ignore";
|
|
2885
|
+
if (fileStat.size > maxSize) return "skip";
|
|
2886
|
+
const content = await readFile(absolutePath, "utf-8");
|
|
2887
|
+
return {
|
|
2888
|
+
relativePath: entry,
|
|
2889
|
+
absolutePath,
|
|
2890
|
+
extension: ext,
|
|
2891
|
+
content,
|
|
2892
|
+
sizeBytes: fileStat.size,
|
|
2893
|
+
provenance
|
|
2894
|
+
};
|
|
2895
|
+
} catch {
|
|
2896
|
+
return "skip";
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
function resolveOptions(options) {
|
|
2900
|
+
return {
|
|
2901
|
+
targetPath: resolve(options.targetPath),
|
|
2902
|
+
scope: options.scope ?? DEFAULT_SCOPE,
|
|
2903
|
+
extensions: options.extensions ?? DEFAULT_EXTENSIONS,
|
|
2904
|
+
maxSize: options.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE,
|
|
2905
|
+
maxFiles: options.maxFiles ?? DEFAULT_MAX_FILES,
|
|
2906
|
+
excludeGlobs: options.excludeGlobs
|
|
2907
|
+
};
|
|
2908
|
+
}
|
|
2909
|
+
async function readDirectoryEntries(targetPath) {
|
|
2910
|
+
try {
|
|
2911
|
+
return await readdir(targetPath, { recursive: true, encoding: "utf-8" });
|
|
2912
|
+
} catch (err) {
|
|
2913
|
+
throw new Error(`Cannot read directory: ${targetPath}: ${err}`);
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
function processEntry(entry, ig, targetPath, filterOpts) {
|
|
2917
|
+
if (ig.ignores(entry)) return "ignore";
|
|
2918
|
+
return shouldProcessEntry(entry, targetPath, filterOpts);
|
|
2919
|
+
}
|
|
2920
|
+
async function discoverFiles(options) {
|
|
2921
|
+
const { targetPath, scope, extensions, maxSize, maxFiles, excludeGlobs } = resolveOptions(options);
|
|
2922
|
+
const ig = await loadGitignore(targetPath);
|
|
2923
|
+
if (excludeGlobs?.length) ig.add(excludeGlobs);
|
|
2924
|
+
const entries = await readDirectoryEntries(targetPath);
|
|
2925
|
+
const filterOpts = { scope, extensions, maxSize };
|
|
2926
|
+
const files = [];
|
|
2927
|
+
let skippedCount = 0;
|
|
2928
|
+
for (const entry of entries) {
|
|
2929
|
+
if (files.length >= maxFiles) {
|
|
2930
|
+
skippedCount++;
|
|
2931
|
+
continue;
|
|
2932
|
+
}
|
|
2933
|
+
const processed = processEntry(entry, ig, targetPath, filterOpts);
|
|
2934
|
+
if (processed === "skip") {
|
|
2935
|
+
skippedCount++;
|
|
2936
|
+
continue;
|
|
2937
|
+
}
|
|
2938
|
+
if (processed === "ignore") continue;
|
|
2939
|
+
const result = await tryReadFile(
|
|
2940
|
+
processed.absolutePath,
|
|
2941
|
+
entry,
|
|
2942
|
+
processed.ext,
|
|
2943
|
+
processed.provenance,
|
|
2944
|
+
maxSize
|
|
2945
|
+
);
|
|
2946
|
+
if (result === "skip") {
|
|
2947
|
+
skippedCount++;
|
|
2948
|
+
continue;
|
|
2949
|
+
}
|
|
2950
|
+
if (result === "ignore") continue;
|
|
2951
|
+
files.push(result);
|
|
2952
|
+
}
|
|
2953
|
+
return { files, skippedCount };
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
// ../core/src/scanner/aggregator.ts
|
|
2957
|
+
var TOOL_VERSION = "0.1.0";
|
|
2958
|
+
function parseRootPackageJson(files) {
|
|
2959
|
+
const pkgFile = files.find((f) => f.relativePath === "package.json");
|
|
2960
|
+
if (!pkgFile) return void 0;
|
|
2961
|
+
try {
|
|
2962
|
+
return JSON.parse(pkgFile.content);
|
|
2963
|
+
} catch {
|
|
2964
|
+
return void 0;
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
function buildRepoProfile(targetPath, pkg, files) {
|
|
2968
|
+
const hasTs = files.some((f) => f.extension === ".ts" || f.extension === ".tsx");
|
|
2969
|
+
const hasJs = files.some((f) => f.extension === ".js" || f.extension === ".jsx");
|
|
2970
|
+
const languages = [];
|
|
2971
|
+
if (hasTs) languages.push("typescript");
|
|
2972
|
+
if (hasJs) languages.push("javascript");
|
|
2973
|
+
let packageManager;
|
|
2974
|
+
if (files.some((f) => f.relativePath === "pnpm-lock.yaml")) packageManager = "pnpm";
|
|
2975
|
+
else if (files.some((f) => f.relativePath === "yarn.lock")) packageManager = "yarn";
|
|
2976
|
+
else if (files.some((f) => f.relativePath === "package-lock.json")) packageManager = "npm";
|
|
2977
|
+
const isMonorepo = Boolean(
|
|
2978
|
+
pkg?.workspaces || files.some((f) => f.relativePath === "pnpm-workspace.yaml")
|
|
2979
|
+
);
|
|
2980
|
+
return {
|
|
2981
|
+
name: pkg?.name ?? basename3(targetPath),
|
|
2982
|
+
version: pkg?.version,
|
|
2983
|
+
description: pkg?.description,
|
|
2984
|
+
license: pkg?.license,
|
|
2985
|
+
isMonorepo,
|
|
2986
|
+
packageManager,
|
|
2987
|
+
languages
|
|
2988
|
+
};
|
|
2989
|
+
}
|
|
2990
|
+
function inferAppTypes(signals) {
|
|
2991
|
+
const types = /* @__PURE__ */ new Set();
|
|
2992
|
+
const has = (id) => signals.some((s) => s.id === id);
|
|
2993
|
+
const hasCategory = (cat) => signals.some((s) => s.category === cat);
|
|
2994
|
+
if (has("framework-nextjs") || has("framework-react")) types.add("web-app");
|
|
2995
|
+
if (has("framework-express") || has("framework-fastify") || has("framework-nestjs") || has("framework-hono") || has("framework-elysia"))
|
|
2996
|
+
types.add("api-server");
|
|
2997
|
+
if (hasCategory("local-inference") || hasCategory("ai-provider")) {
|
|
2998
|
+
if (signals.some((s) => s.name.toLowerCase().includes("chat"))) types.add("chatbot");
|
|
2999
|
+
}
|
|
3000
|
+
if (hasCategory("training")) types.add("ml-pipeline");
|
|
3001
|
+
if (types.size === 0) types.add("unknown");
|
|
3002
|
+
return [...types];
|
|
3003
|
+
}
|
|
3004
|
+
function deduplicateSignals(signals) {
|
|
3005
|
+
const map = /* @__PURE__ */ new Map();
|
|
3006
|
+
for (const signal of signals) {
|
|
3007
|
+
const existing = map.get(signal.id);
|
|
3008
|
+
if (!existing) {
|
|
3009
|
+
map.set(signal.id, { ...signal, evidence: [...signal.evidence] });
|
|
3010
|
+
continue;
|
|
3011
|
+
}
|
|
3012
|
+
const seen = new Set(existing.evidence.map((e) => `${e.file}:${e.line ?? ""}`));
|
|
3013
|
+
for (const ev of signal.evidence) {
|
|
3014
|
+
const key = `${ev.file}:${ev.line ?? ""}`;
|
|
3015
|
+
if (!seen.has(key)) {
|
|
3016
|
+
seen.add(key);
|
|
3017
|
+
existing.evidence.push(ev);
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
if (CONFIDENCE_RANK[signal.confidence] > CONFIDENCE_RANK[existing.confidence]) {
|
|
3021
|
+
existing.confidence = signal.confidence;
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
return [...map.values()];
|
|
3025
|
+
}
|
|
3026
|
+
function annotateEvidenceProvenance(signals, files) {
|
|
3027
|
+
const provenanceByPath = /* @__PURE__ */ new Map();
|
|
3028
|
+
for (const file of files) {
|
|
3029
|
+
provenanceByPath.set(file.relativePath, file.provenance);
|
|
3030
|
+
}
|
|
3031
|
+
return signals.map((signal) => ({
|
|
3032
|
+
...signal,
|
|
3033
|
+
evidence: signal.evidence.map((evidence) => ({
|
|
3034
|
+
...evidence,
|
|
3035
|
+
provenance: evidence.provenance ?? provenanceByPath.get(evidence.file) ?? "runtime"
|
|
3036
|
+
}))
|
|
3037
|
+
}));
|
|
3038
|
+
}
|
|
3039
|
+
function generateOpenQuestions(signals) {
|
|
3040
|
+
const questions = [];
|
|
3041
|
+
const hasAI2 = signals.some(
|
|
3042
|
+
(s) => s.category === "ai-provider" || s.category === "ai-framework" || s.category === "local-inference"
|
|
3043
|
+
);
|
|
3044
|
+
if (hasAI2) {
|
|
3045
|
+
questions.push({
|
|
3046
|
+
id: "oq-market-placement",
|
|
3047
|
+
question: "Is this AI system placed on the EU market or used within the EU?",
|
|
3048
|
+
context: "The EU AI Act applies to AI systems placed on or put into service in the EU market.",
|
|
3049
|
+
relatedSignalIds: signals.filter((s) => s.category.startsWith("ai-")).map((s) => s.id),
|
|
3050
|
+
suggestedAction: "Determine your system's geographic scope and market placement."
|
|
3051
|
+
});
|
|
3052
|
+
questions.push({
|
|
3053
|
+
id: "oq-intended-purpose",
|
|
3054
|
+
question: "What is the intended purpose of this AI system?",
|
|
3055
|
+
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.",
|
|
3056
|
+
relatedSignalIds: [],
|
|
3057
|
+
suggestedAction: "Use the EuConform wizard to complete your risk assessment with human-provided context."
|
|
3058
|
+
});
|
|
3059
|
+
questions.push({
|
|
3060
|
+
id: "oq-provider-role",
|
|
3061
|
+
question: "Are you a provider, deployer, or importer of this AI system?",
|
|
3062
|
+
context: "Different roles under Art. 3 carry different obligations. A scanner cannot determine your organizational role.",
|
|
3063
|
+
relatedSignalIds: [],
|
|
3064
|
+
suggestedAction: "Clarify your role in the AI value chain to determine applicable obligations."
|
|
3065
|
+
});
|
|
3066
|
+
}
|
|
3067
|
+
const hasGpaiSignals = signals.some(
|
|
3068
|
+
(s) => s.category === "ai-provider" || s.id.includes("langchain") || s.id.includes("llamaindex")
|
|
3069
|
+
);
|
|
3070
|
+
if (hasGpaiSignals) {
|
|
3071
|
+
questions.push({
|
|
3072
|
+
id: "oq-gpai-downstream",
|
|
3073
|
+
question: "Are you using a general-purpose AI model? Do you provide downstream access to others?",
|
|
3074
|
+
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.",
|
|
3075
|
+
relatedSignalIds: signals.filter((s) => s.category === "ai-provider").map((s) => s.id),
|
|
3076
|
+
suggestedAction: "Determine whether you are a GPAI provider or deployer."
|
|
3077
|
+
});
|
|
3078
|
+
}
|
|
3079
|
+
const hasNoDisclosure = !hasImplementationSignals(signals, ["compliance-disclosure"]);
|
|
3080
|
+
if (hasAI2 && hasNoDisclosure) {
|
|
3081
|
+
questions.push({
|
|
3082
|
+
id: "oq-disclosure-missing",
|
|
3083
|
+
question: "Does your system inform users that they are interacting with AI?",
|
|
3084
|
+
context: "No AI disclosure signals were found in the codebase. Art. 50 requires transparency for AI systems interacting with natural persons.",
|
|
3085
|
+
relatedSignalIds: [],
|
|
3086
|
+
suggestedAction: "Add explicit AI disclosure in user-facing entry points."
|
|
3087
|
+
});
|
|
3088
|
+
}
|
|
3089
|
+
return questions;
|
|
3090
|
+
}
|
|
3091
|
+
function buildSignalSummary(signals) {
|
|
3092
|
+
const byCategory = {};
|
|
3093
|
+
const byConfidence = {
|
|
3094
|
+
high: 0,
|
|
3095
|
+
medium: 0,
|
|
3096
|
+
low: 0
|
|
3097
|
+
};
|
|
3098
|
+
for (const s of signals) {
|
|
3099
|
+
byCategory[s.category] = (byCategory[s.category] ?? 0) + 1;
|
|
3100
|
+
byConfidence[s.confidence]++;
|
|
3101
|
+
}
|
|
3102
|
+
return { total: signals.length, byCategory, byConfidence };
|
|
3103
|
+
}
|
|
3104
|
+
async function scanRepository(options) {
|
|
3105
|
+
const startTime = Date.now();
|
|
3106
|
+
const scope = options.scope ?? "production";
|
|
3107
|
+
const discovery = await discoverFiles(options);
|
|
3108
|
+
const allFiles = discovery.files;
|
|
3109
|
+
const rootPkg = parseRootPackageJson(allFiles);
|
|
3110
|
+
const rawSignals = [];
|
|
3111
|
+
for (const file of allFiles) {
|
|
3112
|
+
const ctx = { file, lines: file.content.split("\n"), allFiles, rootPackageJson: rootPkg };
|
|
3113
|
+
rawSignals.push(...runAllDetectors(ctx));
|
|
3114
|
+
}
|
|
3115
|
+
const signals = annotateEvidenceProvenance(deduplicateSignals(rawSignals), allFiles);
|
|
3116
|
+
const openQuestions = generateOpenQuestions(signals);
|
|
3117
|
+
return {
|
|
3118
|
+
meta: {
|
|
3119
|
+
scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3120
|
+
scanDurationMs: Date.now() - startTime,
|
|
3121
|
+
toolVersion: TOOL_VERSION,
|
|
3122
|
+
targetPath: options.targetPath,
|
|
3123
|
+
scanScope: scope,
|
|
3124
|
+
filesScanned: allFiles.length,
|
|
3125
|
+
filesSkipped: discovery.skippedCount
|
|
3126
|
+
},
|
|
3127
|
+
repo: buildRepoProfile(options.targetPath, rootPkg, allFiles),
|
|
3128
|
+
appTypes: inferAppTypes(signals),
|
|
3129
|
+
signals,
|
|
3130
|
+
openQuestions,
|
|
3131
|
+
signalSummary: buildSignalSummary(signals)
|
|
3132
|
+
};
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
// src/commands/scan.ts
|
|
3136
|
+
import { defineCommand } from "citty";
|
|
3137
|
+
import consola4 from "consola";
|
|
3138
|
+
|
|
3139
|
+
// src/output/ci.ts
|
|
3140
|
+
import { appendFile, mkdir, writeFile } from "fs/promises";
|
|
3141
|
+
import { join as join2 } from "path";
|
|
3142
|
+
import consola from "consola";
|
|
3143
|
+
|
|
3144
|
+
// src/utils/gap-priority.ts
|
|
3145
|
+
var GAP_PRIORITY_ORDER = {
|
|
3146
|
+
critical: 0,
|
|
3147
|
+
high: 1,
|
|
3148
|
+
medium: 2,
|
|
3149
|
+
low: 3
|
|
3150
|
+
};
|
|
3151
|
+
function shouldFailOnGaps(gaps, failOn) {
|
|
3152
|
+
if (failOn === "none") return false;
|
|
3153
|
+
const threshold = GAP_PRIORITY_ORDER[failOn];
|
|
3154
|
+
return gaps.some((gap) => GAP_PRIORITY_ORDER[gap.priority] <= threshold);
|
|
3155
|
+
}
|
|
3156
|
+
|
|
3157
|
+
// src/output/ci.ts
|
|
3158
|
+
var PRIORITY_LABELS = {
|
|
3159
|
+
critical: "Critical",
|
|
3160
|
+
high: "High",
|
|
3161
|
+
medium: "Medium",
|
|
3162
|
+
low: "Low"
|
|
3163
|
+
};
|
|
3164
|
+
function countGaps(gaps) {
|
|
3165
|
+
return gaps.reduce(
|
|
3166
|
+
(counts, gap) => {
|
|
3167
|
+
counts[gap.priority]++;
|
|
3168
|
+
return counts;
|
|
3169
|
+
},
|
|
3170
|
+
{ critical: 0, high: 0, medium: 0, low: 0 }
|
|
3171
|
+
);
|
|
3172
|
+
}
|
|
3173
|
+
function escapeGitHubValue(value) {
|
|
3174
|
+
return value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
3175
|
+
}
|
|
3176
|
+
function annotationLevel(priority) {
|
|
3177
|
+
if (priority === "critical" || priority === "high") return "error";
|
|
3178
|
+
if (priority === "medium") return "warning";
|
|
3179
|
+
return "notice";
|
|
3180
|
+
}
|
|
3181
|
+
function buildCiReport(output, failOn, scanScope, baseArtifacts) {
|
|
3182
|
+
const gapCounts = countGaps(output.report.gaps);
|
|
3183
|
+
const complianceAreas = [
|
|
3184
|
+
{ area: "Disclosure", group: output.report.complianceSignals.disclosure },
|
|
3185
|
+
{ area: "Bias Testing", group: output.report.complianceSignals.biasTesting },
|
|
3186
|
+
{ area: "Reporting & Exports", group: output.report.complianceSignals.reportingExports },
|
|
3187
|
+
{ area: "Logging & Monitoring", group: output.report.complianceSignals.loggingMonitoring },
|
|
3188
|
+
{ area: "Human Oversight", group: output.report.complianceSignals.humanOversight },
|
|
3189
|
+
{ area: "Data Governance", group: output.report.complianceSignals.dataGovernance },
|
|
3190
|
+
{ area: "Incident Reporting", group: output.report.complianceSignals.incidentReporting }
|
|
3191
|
+
];
|
|
3192
|
+
return {
|
|
3193
|
+
schemaVersion: "euconform.ci.v1",
|
|
3194
|
+
generatedAt: output.report.generatedAt,
|
|
3195
|
+
target: {
|
|
3196
|
+
name: output.report.target.name,
|
|
3197
|
+
rootPath: output.report.target.rootPath
|
|
3198
|
+
},
|
|
3199
|
+
status: {
|
|
3200
|
+
failOn,
|
|
3201
|
+
failing: shouldFailOnGaps(output.report.gaps, failOn),
|
|
3202
|
+
gapCounts,
|
|
3203
|
+
openQuestions: output.report.assessmentHints.openQuestions.length
|
|
3204
|
+
},
|
|
3205
|
+
aiDetected: output.report.aiFootprint.usesAI,
|
|
3206
|
+
scanScope,
|
|
3207
|
+
artifacts: [
|
|
3208
|
+
...baseArtifacts,
|
|
3209
|
+
"euconform.bundle.json",
|
|
3210
|
+
"euconform.ci.json",
|
|
3211
|
+
"euconform.ci-summary.md"
|
|
3212
|
+
],
|
|
3213
|
+
complianceOverview: complianceAreas.map(({ area, group }) => ({
|
|
3214
|
+
area,
|
|
3215
|
+
status: group.status,
|
|
3216
|
+
confidence: group.confidence
|
|
3217
|
+
})),
|
|
3218
|
+
topGaps: output.report.gaps.slice(0, 5).map((gap) => ({
|
|
3219
|
+
id: gap.id,
|
|
3220
|
+
title: gap.title,
|
|
3221
|
+
priority: gap.priority,
|
|
3222
|
+
status: gap.status
|
|
3223
|
+
}))
|
|
3224
|
+
};
|
|
3225
|
+
}
|
|
3226
|
+
function renderCiSummaryMarkdown(output, report) {
|
|
3227
|
+
const lines = [
|
|
3228
|
+
"## EuConform Compliance Scan",
|
|
3229
|
+
"",
|
|
3230
|
+
`- **Project:** ${report.target.name}`,
|
|
3231
|
+
`- **Scan Scope:** ${report.scanScope}`,
|
|
3232
|
+
`- **AI Detected:** ${report.aiDetected ? "yes" : "no"}`,
|
|
3233
|
+
`- **Fail Threshold:** ${report.status.failOn}`,
|
|
3234
|
+
`- **CI Status:** ${report.status.failing ? "failing" : "passing"}`,
|
|
3235
|
+
"",
|
|
3236
|
+
"### Gap Counts",
|
|
3237
|
+
"",
|
|
3238
|
+
`- Critical: ${report.status.gapCounts.critical}`,
|
|
3239
|
+
`- High: ${report.status.gapCounts.high}`,
|
|
3240
|
+
`- Medium: ${report.status.gapCounts.medium}`,
|
|
3241
|
+
`- Low: ${report.status.gapCounts.low}`,
|
|
3242
|
+
"",
|
|
3243
|
+
"### Compliance Overview",
|
|
3244
|
+
"",
|
|
3245
|
+
"| Area | Status | Confidence |",
|
|
3246
|
+
"|------|--------|------------|",
|
|
3247
|
+
...report.complianceOverview.map(
|
|
3248
|
+
(item) => `| ${item.area} | ${item.status} | ${item.confidence} |`
|
|
3249
|
+
),
|
|
3250
|
+
""
|
|
3251
|
+
];
|
|
3252
|
+
if (report.topGaps.length > 0) {
|
|
3253
|
+
lines.push("### Top Gaps", "");
|
|
3254
|
+
for (const gap of report.topGaps) {
|
|
3255
|
+
lines.push(`- **${PRIORITY_LABELS[gap.priority]}** ${gap.title}`);
|
|
3256
|
+
}
|
|
3257
|
+
lines.push("");
|
|
3258
|
+
}
|
|
3259
|
+
if (output.report.assessmentHints.openQuestions.length > 0) {
|
|
3260
|
+
lines.push("### Open Questions", "");
|
|
3261
|
+
for (const question of output.report.assessmentHints.openQuestions.slice(0, 5)) {
|
|
3262
|
+
lines.push(`- ${question}`);
|
|
3263
|
+
}
|
|
3264
|
+
lines.push("");
|
|
3265
|
+
}
|
|
3266
|
+
lines.push("### Artifacts", "", ...report.artifacts.map((artifact) => `- \`${artifact}\``));
|
|
3267
|
+
return `${lines.join("\n")}
|
|
3268
|
+
`;
|
|
3269
|
+
}
|
|
3270
|
+
function emitGitHubAnnotations(output) {
|
|
3271
|
+
for (const gap of output.report.gaps.slice(0, 10)) {
|
|
3272
|
+
const level = annotationLevel(gap.priority);
|
|
3273
|
+
const firstEvidence = gap.evidence[0];
|
|
3274
|
+
const message = escapeGitHubValue(`EuConform: ${gap.title}`);
|
|
3275
|
+
if (firstEvidence?.file) {
|
|
3276
|
+
const line = firstEvidence.line ? `,line=${firstEvidence.line}` : "";
|
|
3277
|
+
process.stdout.write(`::${level} file=${firstEvidence.file}${line}::${message}
|
|
3278
|
+
`);
|
|
3279
|
+
} else {
|
|
3280
|
+
process.stdout.write(`::${level}::${message}
|
|
3281
|
+
`);
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
for (const question of output.report.assessmentHints.openQuestions.slice(0, 5)) {
|
|
3285
|
+
process.stdout.write(
|
|
3286
|
+
`::notice::${escapeGitHubValue(`EuConform open question: ${question}`)}
|
|
3287
|
+
`
|
|
3288
|
+
);
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
async function writeCiArtifacts(output, outputDir, failOn, mode, scanScope, baseArtifacts) {
|
|
3292
|
+
if (mode === "off") return null;
|
|
3293
|
+
await mkdir(outputDir, { recursive: true });
|
|
3294
|
+
const ciReport = buildCiReport(output, failOn, scanScope, baseArtifacts);
|
|
3295
|
+
const reportPath = join2(outputDir, "euconform.ci.json");
|
|
3296
|
+
const summaryPath = join2(outputDir, "euconform.ci-summary.md");
|
|
3297
|
+
await writeFile(reportPath, `${JSON.stringify(ciReport, null, 2)}
|
|
3298
|
+
`, "utf-8");
|
|
3299
|
+
await writeFile(summaryPath, renderCiSummaryMarkdown(output, ciReport), "utf-8");
|
|
3300
|
+
consola.success(`Written ${reportPath}`);
|
|
3301
|
+
consola.success(`Written ${summaryPath}`);
|
|
3302
|
+
let githubStepSummaryPath;
|
|
3303
|
+
if (mode === "github") {
|
|
3304
|
+
emitGitHubAnnotations(output);
|
|
3305
|
+
const stepSummaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
3306
|
+
if (stepSummaryPath) {
|
|
3307
|
+
await appendFile(stepSummaryPath, `
|
|
3308
|
+
${renderCiSummaryMarkdown(output, ciReport)}`, "utf-8");
|
|
3309
|
+
githubStepSummaryPath = stepSummaryPath;
|
|
3310
|
+
consola.info(`Appended GitHub step summary: ${stepSummaryPath}`);
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
return { reportPath, summaryPath, githubStepSummaryPath, baseArtifacts };
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
// src/output/terminal.ts
|
|
3317
|
+
import consola2 from "consola";
|
|
3318
|
+
var AREA_LABELS = {
|
|
3319
|
+
disclosure: "Disclosure & Transparency",
|
|
3320
|
+
biasTesting: "Bias Testing",
|
|
3321
|
+
reportingExports: "Reporting & Exports",
|
|
3322
|
+
loggingMonitoring: "Logging & Monitoring",
|
|
3323
|
+
humanOversight: "Human Oversight",
|
|
3324
|
+
dataGovernance: "Data Governance",
|
|
3325
|
+
incidentReporting: "Incident Reporting"
|
|
3326
|
+
};
|
|
3327
|
+
var STATUS_ICONS = {
|
|
3328
|
+
present: "[OK]",
|
|
3329
|
+
partial: "[!!]",
|
|
3330
|
+
absent: "[--]"
|
|
3331
|
+
};
|
|
3332
|
+
function printSignalCounts(complianceSignals) {
|
|
3333
|
+
const byConfidence = { high: 0, medium: 0, low: 0 };
|
|
3334
|
+
for (const area of Object.values(complianceSignals)) {
|
|
3335
|
+
byConfidence[area.confidence]++;
|
|
3336
|
+
}
|
|
3337
|
+
const total = Object.values(complianceSignals).length;
|
|
3338
|
+
consola2.info(
|
|
3339
|
+
`${total} compliance areas assessed (${byConfidence.high} high, ${byConfidence.medium} medium, ${byConfidence.low} low confidence)`
|
|
3340
|
+
);
|
|
3341
|
+
}
|
|
3342
|
+
function printComplianceOverview(complianceSignals) {
|
|
3343
|
+
consola2.log("");
|
|
3344
|
+
consola2.info("Compliance Overview:");
|
|
3345
|
+
for (const [key, group] of Object.entries(complianceSignals)) {
|
|
3346
|
+
const label = AREA_LABELS[key] ?? key;
|
|
3347
|
+
const icon = STATUS_ICONS[group.status] ?? "[??]";
|
|
3348
|
+
consola2.log(` ${icon} ${label.padEnd(28)} ${group.status} (${group.confidence})`);
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
function printTopGaps(gaps) {
|
|
3352
|
+
if (gaps.length === 0) return;
|
|
3353
|
+
consola2.log("");
|
|
3354
|
+
consola2.warn(`Top gaps (${gaps.length} total):`);
|
|
3355
|
+
for (const gap of gaps.slice(0, 3)) {
|
|
3356
|
+
consola2.log(` - [${gap.priority}] ${gap.title}`);
|
|
3357
|
+
}
|
|
3358
|
+
if (gaps.length > 3) {
|
|
3359
|
+
consola2.log(` ... and ${gaps.length - 3} more`);
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
function printTerminalSummary(output, outputDir) {
|
|
3363
|
+
const { report } = output;
|
|
3364
|
+
consola2.box("EuConform Scan Complete");
|
|
3365
|
+
printSignalCounts(report.complianceSignals);
|
|
3366
|
+
const ai = report.aiFootprint;
|
|
3367
|
+
consola2.info(
|
|
3368
|
+
`AI detected: ${ai.usesAI ? "yes" : "no"}${ai.inferenceModes.length > 0 ? ` | Inference: ${ai.inferenceModes.join(", ")}` : ""}`
|
|
3369
|
+
);
|
|
3370
|
+
printComplianceOverview(report.complianceSignals);
|
|
3371
|
+
printTopGaps(report.gaps);
|
|
3372
|
+
const openCount = report.assessmentHints.openQuestions.length;
|
|
3373
|
+
if (openCount > 0) {
|
|
3374
|
+
consola2.info(`${openCount} open question${openCount > 1 ? "s" : ""} for manual review`);
|
|
3375
|
+
}
|
|
3376
|
+
consola2.log("");
|
|
3377
|
+
consola2.success(`Output written to: ${outputDir}`);
|
|
3378
|
+
consola2.info("Next steps:");
|
|
3379
|
+
consola2.log(" - Review `euconform.report.json` and `euconform.summary.md`");
|
|
3380
|
+
consola2.log(" - Complete role and risk classification in the EuConform web app");
|
|
3381
|
+
consola2.log(" - Use `--ci github --fail-on <level>` to turn scans into CI checks");
|
|
3382
|
+
}
|
|
3383
|
+
|
|
3384
|
+
// src/output/writer.ts
|
|
3385
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
3386
|
+
import { join as join3 } from "path";
|
|
3387
|
+
import consola3 from "consola";
|
|
3388
|
+
async function tryReadArtifact(outputDir, fileName) {
|
|
3389
|
+
try {
|
|
3390
|
+
const content = await readFile2(join3(outputDir, fileName), "utf-8");
|
|
3391
|
+
return { fileName, content };
|
|
3392
|
+
} catch {
|
|
3393
|
+
return null;
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
async function writeBundleManifest(outputDir, metadata) {
|
|
3397
|
+
const report = await tryReadArtifact(outputDir, "euconform.report.json");
|
|
3398
|
+
if (!report) {
|
|
3399
|
+
return null;
|
|
3400
|
+
}
|
|
3401
|
+
const bundle = buildBundleManifest({
|
|
3402
|
+
report,
|
|
3403
|
+
aibom: await tryReadArtifact(outputDir, "euconform.aibom.json") ?? void 0,
|
|
3404
|
+
ci: await tryReadArtifact(outputDir, "euconform.ci.json") ?? void 0,
|
|
3405
|
+
summary: await tryReadArtifact(outputDir, "euconform.summary.md") ?? void 0,
|
|
3406
|
+
tool: metadata.tool,
|
|
3407
|
+
target: metadata.target,
|
|
3408
|
+
generatedAt: metadata.generatedAt
|
|
3409
|
+
});
|
|
3410
|
+
const bundlePath = join3(outputDir, "euconform.bundle.json");
|
|
3411
|
+
await writeFile2(bundlePath, `${JSON.stringify(bundle, null, 2)}
|
|
3412
|
+
`, "utf-8");
|
|
3413
|
+
consola3.success(`Written ${bundlePath}`);
|
|
3414
|
+
return bundlePath;
|
|
3415
|
+
}
|
|
3416
|
+
async function writeOutputFiles(output, outputDir, format) {
|
|
3417
|
+
await mkdir2(outputDir, { recursive: true });
|
|
3418
|
+
const written = [];
|
|
3419
|
+
if (format === "json" || format === "all") {
|
|
3420
|
+
const reportContent = JSON.stringify(output.report, null, 2);
|
|
3421
|
+
const reportPath = join3(outputDir, "euconform.report.json");
|
|
3422
|
+
await writeFile2(reportPath, reportContent, "utf-8");
|
|
3423
|
+
consola3.success(`Written ${reportPath}`);
|
|
3424
|
+
written.push({ fileName: "euconform.report.json", content: reportContent });
|
|
3425
|
+
const aibomContent = JSON.stringify(output.aibom, null, 2);
|
|
3426
|
+
const aibomPath = join3(outputDir, "euconform.aibom.json");
|
|
3427
|
+
await writeFile2(aibomPath, aibomContent, "utf-8");
|
|
3428
|
+
consola3.success(`Written ${aibomPath}`);
|
|
3429
|
+
written.push({ fileName: "euconform.aibom.json", content: aibomContent });
|
|
3430
|
+
}
|
|
3431
|
+
if (format === "md" || format === "all") {
|
|
3432
|
+
const mdPath = join3(outputDir, "euconform.summary.md");
|
|
3433
|
+
await writeFile2(mdPath, output.summaryMarkdown, "utf-8");
|
|
3434
|
+
consola3.success(`Written ${mdPath}`);
|
|
3435
|
+
written.push({ fileName: "euconform.summary.md", content: output.summaryMarkdown });
|
|
3436
|
+
}
|
|
3437
|
+
if (written.some((artifact) => artifact.fileName === "euconform.report.json")) {
|
|
3438
|
+
await writeBundleManifest(outputDir, {
|
|
3439
|
+
tool: output.report.tool,
|
|
3440
|
+
target: {
|
|
3441
|
+
name: output.report.target.name,
|
|
3442
|
+
rootPath: output.report.target.rootPath
|
|
3443
|
+
},
|
|
3444
|
+
generatedAt: output.report.generatedAt
|
|
3445
|
+
});
|
|
3446
|
+
}
|
|
3447
|
+
}
|
|
3448
|
+
async function writeZipBundle(outputDir) {
|
|
3449
|
+
const { zipSync, strToU8 } = await import("fflate");
|
|
3450
|
+
const fileNames = [
|
|
3451
|
+
"euconform.report.json",
|
|
3452
|
+
"euconform.aibom.json",
|
|
3453
|
+
"euconform.summary.md",
|
|
3454
|
+
"euconform.ci.json",
|
|
3455
|
+
"euconform.ci-summary.md",
|
|
3456
|
+
"euconform.bundle.json"
|
|
3457
|
+
];
|
|
3458
|
+
const zipData = {};
|
|
3459
|
+
for (const fileName of fileNames) {
|
|
3460
|
+
try {
|
|
3461
|
+
const content = await readFile2(join3(outputDir, fileName), "utf-8");
|
|
3462
|
+
zipData[fileName] = strToU8(content);
|
|
3463
|
+
} catch {
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
if (!("euconform.bundle.json" in zipData)) {
|
|
3467
|
+
throw new Error("Cannot create euconform.bundle.zip without euconform.bundle.json");
|
|
3468
|
+
}
|
|
3469
|
+
const zipped = zipSync(zipData);
|
|
3470
|
+
const zipPath = join3(outputDir, "euconform.bundle.zip");
|
|
3471
|
+
await writeFile2(zipPath, zipped);
|
|
3472
|
+
consola3.success(`Written ${zipPath}`);
|
|
3473
|
+
return zipPath;
|
|
3474
|
+
}
|
|
3475
|
+
|
|
3476
|
+
// src/commands/scan.ts
|
|
3477
|
+
var VALID_FORMATS = /* @__PURE__ */ new Set(["json", "md", "all"]);
|
|
3478
|
+
var VALID_SCOPES = /* @__PURE__ */ new Set(["production", "all"]);
|
|
3479
|
+
var FAIL_ON_LEVELS = ["none", "critical", "high", "medium", "low"];
|
|
3480
|
+
var VALID_CI_MODES = /* @__PURE__ */ new Set(["off", "github"]);
|
|
3481
|
+
async function validateAndParseArgs(args) {
|
|
3482
|
+
const targetPath = resolve2(args.path);
|
|
3483
|
+
try {
|
|
3484
|
+
const info = await stat2(targetPath);
|
|
3485
|
+
if (!info.isDirectory()) {
|
|
3486
|
+
consola4.error(`Not a directory: ${targetPath}`);
|
|
3487
|
+
process.exit(1);
|
|
3488
|
+
}
|
|
3489
|
+
} catch {
|
|
3490
|
+
consola4.error(`Path does not exist: ${targetPath}`);
|
|
3491
|
+
process.exit(1);
|
|
3492
|
+
}
|
|
3493
|
+
const format = args.format ?? "all";
|
|
3494
|
+
const scope = args.scope ?? "production";
|
|
3495
|
+
const failOn = args["fail-on"] ?? "none";
|
|
3496
|
+
const ciMode = args.ci ?? "off";
|
|
3497
|
+
if (!VALID_FORMATS.has(format)) {
|
|
3498
|
+
consola4.error(`Invalid format: ${format}. Use one of: json, md, all.`);
|
|
3499
|
+
process.exit(1);
|
|
3500
|
+
}
|
|
3501
|
+
if (!VALID_SCOPES.has(scope)) {
|
|
3502
|
+
consola4.error(`Invalid scope: ${scope}. Use one of: production, all.`);
|
|
3503
|
+
process.exit(1);
|
|
3504
|
+
}
|
|
3505
|
+
if (!FAIL_ON_LEVELS.includes(failOn)) {
|
|
3506
|
+
consola4.error(`Invalid fail-on level: ${failOn}. Use one of: ${FAIL_ON_LEVELS.join(", ")}.`);
|
|
3507
|
+
process.exit(1);
|
|
3508
|
+
}
|
|
3509
|
+
if (!VALID_CI_MODES.has(ciMode)) {
|
|
3510
|
+
consola4.error(`Invalid ci mode: ${ciMode}. Use one of: off, github.`);
|
|
3511
|
+
process.exit(1);
|
|
3512
|
+
}
|
|
3513
|
+
if (args.zip && format === "md") {
|
|
3514
|
+
consola4.error('Cannot create euconform.bundle.zip when format is "md" only.');
|
|
3515
|
+
process.exit(1);
|
|
3516
|
+
}
|
|
3517
|
+
if (args.verbose) {
|
|
3518
|
+
consola4.level = 4;
|
|
3519
|
+
}
|
|
3520
|
+
const rawExclude = args["exclude-glob"];
|
|
3521
|
+
const excludeGlobs = rawExclude ? Array.isArray(rawExclude) ? rawExclude : [rawExclude] : void 0;
|
|
3522
|
+
return {
|
|
3523
|
+
targetPath,
|
|
3524
|
+
outputDir: args.output ? resolve2(args.output) : resolve2(targetPath, ".euconform"),
|
|
3525
|
+
format,
|
|
3526
|
+
scope,
|
|
3527
|
+
failOn,
|
|
3528
|
+
ciMode,
|
|
3529
|
+
excludeGlobs
|
|
3530
|
+
};
|
|
3531
|
+
}
|
|
3532
|
+
function buildOutputFileNames(format) {
|
|
3533
|
+
const names = [];
|
|
3534
|
+
if (format === "json" || format === "all") {
|
|
3535
|
+
names.push("euconform.report.json", "euconform.aibom.json");
|
|
3536
|
+
}
|
|
3537
|
+
if (format === "md" || format === "all") {
|
|
3538
|
+
names.push("euconform.summary.md");
|
|
3539
|
+
}
|
|
3540
|
+
return names;
|
|
3541
|
+
}
|
|
3542
|
+
var scan_default = defineCommand({
|
|
3543
|
+
meta: {
|
|
3544
|
+
name: "scan",
|
|
3545
|
+
description: "Scan a project directory for EU AI Act compliance evidence"
|
|
3546
|
+
},
|
|
3547
|
+
args: {
|
|
3548
|
+
path: {
|
|
3549
|
+
type: "positional",
|
|
3550
|
+
required: true,
|
|
3551
|
+
description: "Path to project directory"
|
|
3552
|
+
},
|
|
3553
|
+
output: {
|
|
3554
|
+
type: "string",
|
|
3555
|
+
description: "Output directory (default: <path>/.euconform)"
|
|
3556
|
+
},
|
|
3557
|
+
format: {
|
|
3558
|
+
type: "string",
|
|
3559
|
+
default: "all",
|
|
3560
|
+
description: 'Output format: "json", "md", or "all"'
|
|
3561
|
+
},
|
|
3562
|
+
scope: {
|
|
3563
|
+
type: "string",
|
|
3564
|
+
default: "production",
|
|
3565
|
+
description: 'Scan scope: "production" or "all"'
|
|
3566
|
+
},
|
|
3567
|
+
"fail-on": {
|
|
3568
|
+
type: "string",
|
|
3569
|
+
default: "none",
|
|
3570
|
+
description: 'Exit non-zero when a gap is at or above: "none", "critical", "high", "medium", "low"'
|
|
3571
|
+
},
|
|
3572
|
+
ci: {
|
|
3573
|
+
type: "string",
|
|
3574
|
+
default: "off",
|
|
3575
|
+
description: 'CI output mode: "off" or "github"'
|
|
3576
|
+
},
|
|
3577
|
+
"exclude-glob": {
|
|
3578
|
+
type: "string",
|
|
3579
|
+
description: "Additional exclude patterns (repeatable)"
|
|
3580
|
+
},
|
|
3581
|
+
zip: {
|
|
3582
|
+
type: "boolean",
|
|
3583
|
+
default: false,
|
|
3584
|
+
description: "Create a ZIP archive of all scan artifacts"
|
|
3585
|
+
},
|
|
3586
|
+
verbose: {
|
|
3587
|
+
type: "boolean",
|
|
3588
|
+
default: false,
|
|
3589
|
+
description: "Enable verbose logging"
|
|
3590
|
+
}
|
|
3591
|
+
},
|
|
3592
|
+
async run({ args }) {
|
|
3593
|
+
const { targetPath, outputDir, format, scope, failOn, ciMode, excludeGlobs } = await validateAndParseArgs(args);
|
|
3594
|
+
consola4.start(`Scanning ${targetPath} (${scope} scope)...`);
|
|
3595
|
+
const scanResult = await scanRepository({ targetPath, scope, excludeGlobs });
|
|
3596
|
+
consola4.info(
|
|
3597
|
+
`Found ${scanResult.signals.length} signals across ${scanResult.meta.filesScanned} files`
|
|
3598
|
+
);
|
|
3599
|
+
const output = generateScanOutput(scanResult);
|
|
3600
|
+
await writeOutputFiles(output, outputDir, format);
|
|
3601
|
+
const ciArtifacts = await writeCiArtifacts(
|
|
3602
|
+
output,
|
|
3603
|
+
outputDir,
|
|
3604
|
+
failOn,
|
|
3605
|
+
ciMode,
|
|
3606
|
+
scope,
|
|
3607
|
+
buildOutputFileNames(format)
|
|
3608
|
+
);
|
|
3609
|
+
printTerminalSummary(output, outputDir);
|
|
3610
|
+
if (ciArtifacts) {
|
|
3611
|
+
consola4.info(
|
|
3612
|
+
`CI artifacts ready: ${resolve2(ciArtifacts.reportPath)} and ${resolve2(ciArtifacts.summaryPath)}`
|
|
3613
|
+
);
|
|
3614
|
+
}
|
|
3615
|
+
await writeBundleManifest(outputDir, {
|
|
3616
|
+
tool: output.report.tool,
|
|
3617
|
+
target: {
|
|
3618
|
+
name: output.report.target.name,
|
|
3619
|
+
rootPath: output.report.target.rootPath
|
|
3620
|
+
},
|
|
3621
|
+
generatedAt: output.report.generatedAt
|
|
3622
|
+
});
|
|
3623
|
+
if (args.zip) {
|
|
3624
|
+
await writeZipBundle(outputDir);
|
|
3625
|
+
}
|
|
3626
|
+
if (shouldFailOnGaps(output.report.gaps, failOn)) {
|
|
3627
|
+
consola4.error(`Scan failed CI threshold: found gap(s) at or above "${failOn}".`);
|
|
3628
|
+
process.exit(1);
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
});
|
|
3632
|
+
|
|
3633
|
+
// src/commands/verify.ts
|
|
3634
|
+
import { resolve as resolve4 } from "path";
|
|
3635
|
+
import { defineCommand as defineCommand2 } from "citty";
|
|
3636
|
+
import consola6 from "consola";
|
|
3637
|
+
|
|
3638
|
+
// src/output/verify.ts
|
|
3639
|
+
import consola5 from "consola";
|
|
3640
|
+
function printVerifySummary(report) {
|
|
3641
|
+
consola5.box("EuConform Bundle Verify");
|
|
3642
|
+
consola5.info(`Input: ${report.bundlePath}`);
|
|
3643
|
+
consola5.info(`Mode: ${report.inputType}`);
|
|
3644
|
+
if (!report.bundle) {
|
|
3645
|
+
consola5.error("Bundle manifest could not be validated.");
|
|
3646
|
+
} else {
|
|
3647
|
+
consola5.info(`Bundle: ${report.bundle.schemaVersion}`);
|
|
3648
|
+
consola5.info(
|
|
3649
|
+
`Target: ${report.bundle.target.name} (${report.bundle.tool.name} v${report.bundle.tool.version})`
|
|
3650
|
+
);
|
|
3651
|
+
}
|
|
3652
|
+
if (report.artifacts.length > 0) {
|
|
3653
|
+
consola5.log("");
|
|
3654
|
+
consola5.info("Artifacts:");
|
|
3655
|
+
for (const artifact of report.artifacts) {
|
|
3656
|
+
consola5.log(
|
|
3657
|
+
` - ${artifact.fileName} [${artifact.role}] hash=${artifact.hashStatus} schema=${artifact.schemaStatus} metadata=${artifact.metadataStatus}`
|
|
3658
|
+
);
|
|
3659
|
+
}
|
|
3660
|
+
}
|
|
3661
|
+
if (report.warnings.length > 0) {
|
|
3662
|
+
consola5.log("");
|
|
3663
|
+
consola5.warn(`Warnings (${report.warnings.length}):`);
|
|
3664
|
+
for (const warning of report.warnings) {
|
|
3665
|
+
consola5.warn(` - ${warning.message}`);
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3668
|
+
if (report.errors.length > 0) {
|
|
3669
|
+
consola5.log("");
|
|
3670
|
+
consola5.error(`Errors (${report.errors.length}):`);
|
|
3671
|
+
for (const error of report.errors) {
|
|
3672
|
+
consola5.error(` - ${error.message}`);
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
consola5.log("");
|
|
3676
|
+
if (report.status === "valid") {
|
|
3677
|
+
consola5.success("Bundle verification passed without warnings.");
|
|
3678
|
+
} else if (report.status === "warnings") {
|
|
3679
|
+
consola5.warn("Bundle verification completed with warnings.");
|
|
3680
|
+
} else {
|
|
3681
|
+
consola5.error("Bundle verification failed.");
|
|
3682
|
+
}
|
|
3683
|
+
}
|
|
3684
|
+
|
|
3685
|
+
// src/verify/verify.ts
|
|
3686
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
3687
|
+
import { dirname, join as join4, resolve as resolve3 } from "path";
|
|
3688
|
+
import { strFromU8, unzipSync } from "fflate";
|
|
3689
|
+
function createReport(bundlePath, inputType) {
|
|
3690
|
+
return {
|
|
3691
|
+
bundle: null,
|
|
3692
|
+
bundlePath,
|
|
3693
|
+
inputType,
|
|
3694
|
+
artifacts: [],
|
|
3695
|
+
warnings: [],
|
|
3696
|
+
errors: [],
|
|
3697
|
+
status: "valid"
|
|
3698
|
+
};
|
|
3699
|
+
}
|
|
3700
|
+
function addIssue(report, severity, issue) {
|
|
3701
|
+
const finding = { severity, ...issue };
|
|
3702
|
+
if (severity === "error") {
|
|
3703
|
+
report.errors.push(finding);
|
|
3704
|
+
} else {
|
|
3705
|
+
report.warnings.push(finding);
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
function finalizeReport(report) {
|
|
3709
|
+
report.status = report.errors.length > 0 ? "errors" : report.warnings.length > 0 ? "warnings" : "valid";
|
|
3710
|
+
return report;
|
|
3711
|
+
}
|
|
3712
|
+
async function loadBundleInput(inputPath) {
|
|
3713
|
+
const resolved = resolve3(inputPath);
|
|
3714
|
+
const stats = await import("fs/promises").then(({ stat: stat3 }) => stat3(resolved));
|
|
3715
|
+
if (stats.isDirectory()) {
|
|
3716
|
+
const bundlePath = join4(resolved, "euconform.bundle.json");
|
|
3717
|
+
const manifestContent2 = await readFile3(bundlePath, "utf8");
|
|
3718
|
+
return {
|
|
3719
|
+
inputType: "bundle-dir",
|
|
3720
|
+
bundlePath,
|
|
3721
|
+
manifestContent: manifestContent2,
|
|
3722
|
+
artifactLoader: async (fileName) => {
|
|
3723
|
+
try {
|
|
3724
|
+
return await readFile3(join4(resolved, fileName), "utf8");
|
|
3725
|
+
} catch {
|
|
3726
|
+
return null;
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3729
|
+
};
|
|
3730
|
+
}
|
|
3731
|
+
if (resolved.endsWith(".zip")) {
|
|
3732
|
+
const zipData = await readFile3(resolved);
|
|
3733
|
+
const archive = unzipSync(new Uint8Array(zipData));
|
|
3734
|
+
const manifest = archive["euconform.bundle.json"];
|
|
3735
|
+
if (!manifest) {
|
|
3736
|
+
throw new Error("Bundle ZIP does not contain euconform.bundle.json");
|
|
3737
|
+
}
|
|
3738
|
+
return {
|
|
3739
|
+
inputType: "bundle-zip",
|
|
3740
|
+
bundlePath: resolved,
|
|
3741
|
+
manifestContent: strFromU8(manifest),
|
|
3742
|
+
artifactLoader: async (fileName) => {
|
|
3743
|
+
const entry = archive[fileName];
|
|
3744
|
+
return entry ? strFromU8(entry) : null;
|
|
3745
|
+
}
|
|
3746
|
+
};
|
|
3747
|
+
}
|
|
3748
|
+
const manifestContent = await readFile3(resolved, "utf8");
|
|
3749
|
+
return {
|
|
3750
|
+
inputType: "bundle-json",
|
|
3751
|
+
bundlePath: resolved,
|
|
3752
|
+
manifestContent,
|
|
3753
|
+
artifactLoader: async (fileName) => {
|
|
3754
|
+
try {
|
|
3755
|
+
return await readFile3(join4(dirname(resolved), fileName), "utf8");
|
|
3756
|
+
} catch {
|
|
3757
|
+
return null;
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
3760
|
+
};
|
|
3761
|
+
}
|
|
3762
|
+
function compareJsonMetadata(bundle, artifact, document) {
|
|
3763
|
+
const issues = [];
|
|
3764
|
+
if (document.generatedAt !== bundle.generatedAt) {
|
|
3765
|
+
issues.push({
|
|
3766
|
+
artifact: artifact.fileName,
|
|
3767
|
+
code: "metadata.generatedAt",
|
|
3768
|
+
message: `${artifact.fileName} generatedAt does not match bundle manifest`
|
|
3769
|
+
});
|
|
3770
|
+
}
|
|
3771
|
+
if (artifact.role === "report") {
|
|
3772
|
+
const report = document;
|
|
3773
|
+
if (report.tool.name !== bundle.tool.name) {
|
|
3774
|
+
issues.push({
|
|
3775
|
+
artifact: artifact.fileName,
|
|
3776
|
+
code: "metadata.tool.name",
|
|
3777
|
+
message: `${artifact.fileName} tool.name does not match bundle manifest`
|
|
3778
|
+
});
|
|
3779
|
+
}
|
|
3780
|
+
if (report.tool.version !== bundle.tool.version) {
|
|
3781
|
+
issues.push({
|
|
3782
|
+
artifact: artifact.fileName,
|
|
3783
|
+
code: "metadata.tool.version",
|
|
3784
|
+
message: `${artifact.fileName} tool.version does not match bundle manifest`
|
|
3785
|
+
});
|
|
3786
|
+
}
|
|
3787
|
+
if (report.target.name !== bundle.target.name) {
|
|
3788
|
+
issues.push({
|
|
3789
|
+
artifact: artifact.fileName,
|
|
3790
|
+
code: "metadata.target.name",
|
|
3791
|
+
message: `${artifact.fileName} target.name does not match bundle manifest`
|
|
3792
|
+
});
|
|
3793
|
+
}
|
|
3794
|
+
if (report.target.rootPath !== bundle.target.rootPath) {
|
|
3795
|
+
issues.push({
|
|
3796
|
+
artifact: artifact.fileName,
|
|
3797
|
+
code: "metadata.target.rootPath",
|
|
3798
|
+
message: `${artifact.fileName} target.rootPath does not match bundle manifest`
|
|
3799
|
+
});
|
|
3800
|
+
}
|
|
3801
|
+
}
|
|
3802
|
+
if (artifact.role === "aibom") {
|
|
3803
|
+
const aibom = document;
|
|
3804
|
+
if (aibom.project.name !== bundle.target.name) {
|
|
3805
|
+
issues.push({
|
|
3806
|
+
artifact: artifact.fileName,
|
|
3807
|
+
code: "metadata.project.name",
|
|
3808
|
+
message: `${artifact.fileName} project.name does not match bundle target.name`
|
|
3809
|
+
});
|
|
3810
|
+
}
|
|
3811
|
+
if (aibom.project.rootPath !== bundle.target.rootPath) {
|
|
3812
|
+
issues.push({
|
|
3813
|
+
artifact: artifact.fileName,
|
|
3814
|
+
code: "metadata.project.rootPath",
|
|
3815
|
+
message: `${artifact.fileName} project.rootPath does not match bundle target.rootPath`
|
|
3816
|
+
});
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3819
|
+
if (artifact.role === "ci") {
|
|
3820
|
+
const ci = document;
|
|
3821
|
+
if (ci.target.name !== bundle.target.name) {
|
|
3822
|
+
issues.push({
|
|
3823
|
+
artifact: artifact.fileName,
|
|
3824
|
+
code: "metadata.target.name",
|
|
3825
|
+
message: `${artifact.fileName} target.name does not match bundle manifest`
|
|
3826
|
+
});
|
|
3827
|
+
}
|
|
3828
|
+
if (ci.target.rootPath !== bundle.target.rootPath) {
|
|
3829
|
+
issues.push({
|
|
3830
|
+
artifact: artifact.fileName,
|
|
3831
|
+
code: "metadata.target.rootPath",
|
|
3832
|
+
message: `${artifact.fileName} target.rootPath does not match bundle manifest`
|
|
3833
|
+
});
|
|
3834
|
+
}
|
|
3835
|
+
}
|
|
3836
|
+
return issues;
|
|
3837
|
+
}
|
|
3838
|
+
function validateJsonArtifactForRole(artifact, parsed) {
|
|
3839
|
+
switch (artifact.role) {
|
|
3840
|
+
case "report":
|
|
3841
|
+
return validateScanReport(parsed);
|
|
3842
|
+
case "aibom":
|
|
3843
|
+
return validateAiBillOfMaterials(parsed);
|
|
3844
|
+
case "ci":
|
|
3845
|
+
return validateCiReport(parsed);
|
|
3846
|
+
default:
|
|
3847
|
+
throw new Error(`Unsupported JSON artifact role '${artifact.role}'`);
|
|
3848
|
+
}
|
|
3849
|
+
}
|
|
3850
|
+
function metadataSeverity(strict) {
|
|
3851
|
+
return strict ? "error" : "warning";
|
|
3852
|
+
}
|
|
3853
|
+
async function verifyBundleInput(inputPath, options = {}) {
|
|
3854
|
+
const loaded = await loadBundleInput(inputPath);
|
|
3855
|
+
const report = createReport(loaded.bundlePath, loaded.inputType);
|
|
3856
|
+
let parsedManifest;
|
|
3857
|
+
try {
|
|
3858
|
+
parsedManifest = JSON.parse(loaded.manifestContent);
|
|
3859
|
+
} catch {
|
|
3860
|
+
addIssue(report, "error", {
|
|
3861
|
+
code: "bundle.parse",
|
|
3862
|
+
message: "Failed to parse euconform.bundle.json as JSON"
|
|
3863
|
+
});
|
|
3864
|
+
return finalizeReport(report);
|
|
3865
|
+
}
|
|
3866
|
+
let bundle;
|
|
3867
|
+
try {
|
|
3868
|
+
bundle = validateScanBundle(parsedManifest);
|
|
3869
|
+
} catch (error) {
|
|
3870
|
+
addIssue(report, "error", {
|
|
3871
|
+
code: "bundle.invalid",
|
|
3872
|
+
message: error instanceof Error ? error.message : String(error)
|
|
3873
|
+
});
|
|
3874
|
+
return finalizeReport(report);
|
|
3875
|
+
}
|
|
3876
|
+
report.bundle = {
|
|
3877
|
+
schemaVersion: bundle.schemaVersion,
|
|
3878
|
+
generatedAt: bundle.generatedAt,
|
|
3879
|
+
tool: bundle.tool,
|
|
3880
|
+
target: bundle.target
|
|
3881
|
+
};
|
|
3882
|
+
const bundleMajor = getSchemaMajorVersion(bundle.schemaVersion);
|
|
3883
|
+
for (const artifact of bundle.artifacts) {
|
|
3884
|
+
const result = {
|
|
3885
|
+
role: artifact.role,
|
|
3886
|
+
fileName: artifact.fileName,
|
|
3887
|
+
required: artifact.required,
|
|
3888
|
+
schemaVersion: artifact.schemaVersion,
|
|
3889
|
+
hashStatus: "missing",
|
|
3890
|
+
schemaStatus: artifact.role === "summary" ? "skipped" : "invalid",
|
|
3891
|
+
metadataStatus: "skipped"
|
|
3892
|
+
};
|
|
3893
|
+
const content = await loaded.artifactLoader(artifact.fileName);
|
|
3894
|
+
if (content === null) {
|
|
3895
|
+
result.hashStatus = "missing";
|
|
3896
|
+
result.schemaStatus = artifact.role === "summary" ? "skipped" : "invalid";
|
|
3897
|
+
result.metadataStatus = "skipped";
|
|
3898
|
+
report.artifacts.push(result);
|
|
3899
|
+
addIssue(report, "error", {
|
|
3900
|
+
artifact: artifact.fileName,
|
|
3901
|
+
code: "artifact.missing",
|
|
3902
|
+
message: `Referenced artifact '${artifact.fileName}' is missing`
|
|
3903
|
+
});
|
|
3904
|
+
continue;
|
|
3905
|
+
}
|
|
3906
|
+
if (sha256Hex(content) === artifact.sha256) {
|
|
3907
|
+
result.hashStatus = "match";
|
|
3908
|
+
} else {
|
|
3909
|
+
result.hashStatus = "mismatch";
|
|
3910
|
+
addIssue(report, metadataSeverity(Boolean(options.strict)), {
|
|
3911
|
+
artifact: artifact.fileName,
|
|
3912
|
+
code: "artifact.sha256",
|
|
3913
|
+
message: `${artifact.fileName} does not match the SHA-256 recorded in the bundle`
|
|
3914
|
+
});
|
|
3915
|
+
}
|
|
3916
|
+
if (artifact.role === "summary") {
|
|
3917
|
+
result.schemaStatus = "skipped";
|
|
3918
|
+
result.metadataStatus = "skipped";
|
|
3919
|
+
if (artifact.mimeType && artifact.mimeType !== "text/markdown") {
|
|
3920
|
+
addIssue(report, metadataSeverity(Boolean(options.strict)), {
|
|
3921
|
+
artifact: artifact.fileName,
|
|
3922
|
+
code: "artifact.mimeType",
|
|
3923
|
+
message: `${artifact.fileName} has unexpected mimeType '${artifact.mimeType}'`
|
|
3924
|
+
});
|
|
3925
|
+
}
|
|
3926
|
+
report.artifacts.push(result);
|
|
3927
|
+
continue;
|
|
3928
|
+
}
|
|
3929
|
+
let parsedArtifact;
|
|
3930
|
+
try {
|
|
3931
|
+
parsedArtifact = JSON.parse(content);
|
|
3932
|
+
} catch {
|
|
3933
|
+
result.schemaStatus = "invalid";
|
|
3934
|
+
result.metadataStatus = "skipped";
|
|
3935
|
+
report.artifacts.push(result);
|
|
3936
|
+
addIssue(report, "error", {
|
|
3937
|
+
artifact: artifact.fileName,
|
|
3938
|
+
code: "artifact.parse",
|
|
3939
|
+
message: `${artifact.fileName} is not valid JSON`
|
|
3940
|
+
});
|
|
3941
|
+
continue;
|
|
3942
|
+
}
|
|
3943
|
+
const actualSchemaVersion = parsedArtifact && typeof parsedArtifact === "object" ? parsedArtifact.schemaVersion : void 0;
|
|
3944
|
+
if (typeof actualSchemaVersion !== "string") {
|
|
3945
|
+
result.schemaStatus = "invalid";
|
|
3946
|
+
result.metadataStatus = "skipped";
|
|
3947
|
+
report.artifacts.push(result);
|
|
3948
|
+
addIssue(report, "error", {
|
|
3949
|
+
artifact: artifact.fileName,
|
|
3950
|
+
code: "artifact.schemaVersion",
|
|
3951
|
+
message: `${artifact.fileName} is missing a string schemaVersion`
|
|
3952
|
+
});
|
|
3953
|
+
continue;
|
|
3954
|
+
}
|
|
3955
|
+
result.schemaVersion = actualSchemaVersion;
|
|
3956
|
+
if (artifact.schemaVersion && artifact.schemaVersion !== actualSchemaVersion) {
|
|
3957
|
+
addIssue(report, "error", {
|
|
3958
|
+
artifact: artifact.fileName,
|
|
3959
|
+
code: "artifact.schemaVersion.mismatch",
|
|
3960
|
+
message: `${artifact.fileName} schemaVersion '${actualSchemaVersion}' does not match manifest '${artifact.schemaVersion}'`
|
|
3961
|
+
});
|
|
3962
|
+
}
|
|
3963
|
+
if (bundleMajor && getSchemaMajorVersion(actualSchemaVersion) !== bundleMajor) {
|
|
3964
|
+
addIssue(report, "error", {
|
|
3965
|
+
artifact: artifact.fileName,
|
|
3966
|
+
code: "artifact.schemaVersion.major",
|
|
3967
|
+
message: `${artifact.fileName} uses schemaVersion '${actualSchemaVersion}' with a different major than the bundle`
|
|
3968
|
+
});
|
|
3969
|
+
}
|
|
3970
|
+
let validatedDocument;
|
|
3971
|
+
try {
|
|
3972
|
+
validatedDocument = validateJsonArtifactForRole(artifact, parsedArtifact);
|
|
3973
|
+
result.schemaStatus = "valid";
|
|
3974
|
+
} catch (error) {
|
|
3975
|
+
result.schemaStatus = "invalid";
|
|
3976
|
+
result.metadataStatus = "skipped";
|
|
3977
|
+
report.artifacts.push(result);
|
|
3978
|
+
addIssue(report, "error", {
|
|
3979
|
+
artifact: artifact.fileName,
|
|
3980
|
+
code: "artifact.invalid",
|
|
3981
|
+
message: error instanceof Error ? error.message : String(error)
|
|
3982
|
+
});
|
|
3983
|
+
continue;
|
|
3984
|
+
}
|
|
3985
|
+
const metadataIssues = compareJsonMetadata(bundle, artifact, validatedDocument);
|
|
3986
|
+
if (metadataIssues.length === 0) {
|
|
3987
|
+
result.metadataStatus = "match";
|
|
3988
|
+
} else {
|
|
3989
|
+
result.metadataStatus = "mismatch";
|
|
3990
|
+
for (const issue of metadataIssues) {
|
|
3991
|
+
addIssue(report, metadataSeverity(Boolean(options.strict)), issue);
|
|
3992
|
+
}
|
|
3993
|
+
}
|
|
3994
|
+
report.artifacts.push(result);
|
|
3995
|
+
}
|
|
3996
|
+
return finalizeReport(report);
|
|
3997
|
+
}
|
|
3998
|
+
function shouldFailVerifyReport(report, failOn) {
|
|
3999
|
+
if (report.errors.length > 0) {
|
|
4000
|
+
return true;
|
|
4001
|
+
}
|
|
4002
|
+
if (failOn === "warnings" && report.warnings.length > 0) {
|
|
4003
|
+
return true;
|
|
4004
|
+
}
|
|
4005
|
+
return false;
|
|
4006
|
+
}
|
|
4007
|
+
|
|
4008
|
+
// src/commands/verify.ts
|
|
4009
|
+
var VALID_FAIL_ON = /* @__PURE__ */ new Set(["warnings", "errors"]);
|
|
4010
|
+
var verify_default = defineCommand2({
|
|
4011
|
+
meta: {
|
|
4012
|
+
name: "verify",
|
|
4013
|
+
description: "Verify a EuConform bundle manifest, directory, or ZIP archive"
|
|
4014
|
+
},
|
|
4015
|
+
args: {
|
|
4016
|
+
path: {
|
|
4017
|
+
type: "positional",
|
|
4018
|
+
required: true,
|
|
4019
|
+
description: "Path to euconform.bundle.json, a bundle directory, or euconform.bundle.zip"
|
|
4020
|
+
},
|
|
4021
|
+
strict: {
|
|
4022
|
+
type: "boolean",
|
|
4023
|
+
default: false,
|
|
4024
|
+
description: "Escalate hash and metadata mismatches from warnings to errors"
|
|
4025
|
+
},
|
|
4026
|
+
json: {
|
|
4027
|
+
type: "boolean",
|
|
4028
|
+
default: false,
|
|
4029
|
+
description: "Print the verify report as JSON"
|
|
4030
|
+
},
|
|
4031
|
+
"fail-on": {
|
|
4032
|
+
type: "string",
|
|
4033
|
+
default: "errors",
|
|
4034
|
+
description: 'Exit non-zero on "errors" or on "warnings" and errors'
|
|
4035
|
+
}
|
|
4036
|
+
},
|
|
4037
|
+
async run({ args }) {
|
|
4038
|
+
const inputPath = resolve4(args.path);
|
|
4039
|
+
const failOn = args["fail-on"] ?? "errors";
|
|
4040
|
+
if (!VALID_FAIL_ON.has(failOn)) {
|
|
4041
|
+
consola6.error(`Invalid fail-on level: ${failOn}. Use one of: warnings, errors.`);
|
|
4042
|
+
process.exit(1);
|
|
4043
|
+
}
|
|
4044
|
+
try {
|
|
4045
|
+
const report = await verifyBundleInput(inputPath, { strict: Boolean(args.strict) });
|
|
4046
|
+
if (args.json) {
|
|
4047
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}
|
|
4048
|
+
`);
|
|
4049
|
+
} else {
|
|
4050
|
+
printVerifySummary(report);
|
|
4051
|
+
}
|
|
4052
|
+
if (shouldFailVerifyReport(report, failOn)) {
|
|
4053
|
+
process.exit(1);
|
|
4054
|
+
}
|
|
4055
|
+
} catch (error) {
|
|
4056
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4057
|
+
if (args.json) {
|
|
4058
|
+
process.stdout.write(
|
|
4059
|
+
`${JSON.stringify(
|
|
4060
|
+
{
|
|
4061
|
+
bundlePath: inputPath,
|
|
4062
|
+
inputType: "bundle-json",
|
|
4063
|
+
bundle: null,
|
|
4064
|
+
artifacts: [],
|
|
4065
|
+
warnings: [],
|
|
4066
|
+
errors: [{ severity: "error", code: "verify.load", message }],
|
|
4067
|
+
status: "errors"
|
|
4068
|
+
},
|
|
4069
|
+
null,
|
|
4070
|
+
2
|
|
4071
|
+
)}
|
|
4072
|
+
`
|
|
4073
|
+
);
|
|
4074
|
+
} else {
|
|
4075
|
+
consola6.error(message);
|
|
4076
|
+
}
|
|
4077
|
+
process.exit(1);
|
|
4078
|
+
}
|
|
4079
|
+
}
|
|
4080
|
+
});
|
|
4081
|
+
|
|
4082
|
+
// src/index.ts
|
|
4083
|
+
var main = defineCommand3({
|
|
4084
|
+
meta: {
|
|
4085
|
+
name: "euconform",
|
|
4086
|
+
description: "EU AI Act compliance scanner for local AI projects"
|
|
4087
|
+
},
|
|
4088
|
+
subCommands: {
|
|
4089
|
+
scan: scan_default,
|
|
4090
|
+
verify: verify_default
|
|
4091
|
+
}
|
|
4092
|
+
});
|
|
4093
|
+
runMain(main);
|