@aisdlc/core 2.2.4 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +975 -137
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +148 -2
- package/dist/index.d.ts +148 -2
- package/dist/index.js +961 -138
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -294,19 +294,26 @@ function extractPythonFunctions(content, filePath) {
|
|
|
294
294
|
}
|
|
295
295
|
return buildMetricsFromBlocks(blocks, content, filePath);
|
|
296
296
|
}
|
|
297
|
-
function
|
|
298
|
-
if (
|
|
299
|
-
|
|
300
|
-
|
|
297
|
+
function tryStartBraceFunction(line, state, ext) {
|
|
298
|
+
if (state.currentFn || state.inBlockComment || state.rawTerm) return;
|
|
299
|
+
const candidate = matchFunctionHeader(line, ext);
|
|
300
|
+
if (candidate) {
|
|
301
301
|
state.currentFn = candidate;
|
|
302
302
|
state.braceCount = 0;
|
|
303
303
|
state.fnLines = [];
|
|
304
304
|
}
|
|
305
|
-
|
|
306
|
-
|
|
305
|
+
}
|
|
306
|
+
function updateBraceScanState(line, state, ext) {
|
|
307
|
+
const res = scanBracesInLine(line, state.inBlockComment, state.rawTerm, ext);
|
|
307
308
|
state.inBlockComment = res.nowInBlock;
|
|
309
|
+
state.rawTerm = res.nowRawTerm;
|
|
308
310
|
state.braceCount += res.delta;
|
|
309
|
-
|
|
311
|
+
}
|
|
312
|
+
function processBraceLine(line, state, blocks, lineIndex, ext) {
|
|
313
|
+
tryStartBraceFunction(line, state, ext);
|
|
314
|
+
if (state.currentFn) state.fnLines.push(line);
|
|
315
|
+
updateBraceScanState(line, state, ext);
|
|
316
|
+
if (state.currentFn && state.braceCount === 0 && state.fnLines.length > 1) {
|
|
310
317
|
blocks.push({ name: state.currentFn, body: state.fnLines.join("\n"), startLine: lineIndex });
|
|
311
318
|
state.currentFn = null;
|
|
312
319
|
state.fnLines = [];
|
|
@@ -316,98 +323,153 @@ function extractBraceFunctions(content, filePath) {
|
|
|
316
323
|
const ext = path4__namespace.extname(filePath).toLowerCase();
|
|
317
324
|
const lines = content.split("\n");
|
|
318
325
|
const blocks = [];
|
|
319
|
-
const state = {
|
|
326
|
+
const state = {
|
|
327
|
+
currentFn: null,
|
|
328
|
+
braceCount: 0,
|
|
329
|
+
fnLines: [],
|
|
330
|
+
inBlockComment: false,
|
|
331
|
+
rawTerm: null
|
|
332
|
+
};
|
|
320
333
|
for (let i = 0; i < lines.length; i++) {
|
|
321
334
|
processBraceLine(lines[i], state, blocks, i, ext);
|
|
322
335
|
}
|
|
323
336
|
return buildMetricsFromBlocks(blocks, content, filePath);
|
|
324
337
|
}
|
|
338
|
+
var HEADER_EXCLUSIONS = /* @__PURE__ */ new Set([
|
|
339
|
+
"if",
|
|
340
|
+
"for",
|
|
341
|
+
"foreach",
|
|
342
|
+
"while",
|
|
343
|
+
"switch",
|
|
344
|
+
"catch",
|
|
345
|
+
"select",
|
|
346
|
+
"match",
|
|
347
|
+
"class",
|
|
348
|
+
"struct",
|
|
349
|
+
"interface",
|
|
350
|
+
"record",
|
|
351
|
+
"enum",
|
|
352
|
+
"namespace",
|
|
353
|
+
"using",
|
|
354
|
+
"lock",
|
|
355
|
+
"fixed",
|
|
356
|
+
"synchronized",
|
|
357
|
+
"try",
|
|
358
|
+
"else"
|
|
359
|
+
]);
|
|
325
360
|
function matchCStyleHeader(line) {
|
|
326
361
|
const match = line.match(
|
|
327
|
-
/(?:(?:public|private|protected|static|async|virtual|override|fn)\s+)*(?:function\s+([a-zA-Z0-9_$]+)|(?:[a-zA-Z0-9_<>[\]
|
|
362
|
+
/(?:(?:public|private|protected|internal|static|async|virtual|override|sealed|abstract|final|synchronized|default|fn)\s+)*(?:function\s+([a-zA-Z0-9_$]+)|(?:[a-zA-Z0-9_<>[\]?,.\s]+\s+)+([a-zA-Z0-9_$]+)\s*(?:<[^>]+>)?\s*\([^;{}]*\)\s*(?:where\s+[^{]+|throws\s+[^{]+)?\{|([a-zA-Z0-9_$]+)\s*\([^;{}]*\)\s*(?::\s*[^{]+\s*)?\{)/
|
|
328
363
|
);
|
|
329
364
|
if (!match) return null;
|
|
330
365
|
const candidate = match[1] || match[2] || match[3];
|
|
331
|
-
|
|
332
|
-
return candidate && !exclusions.includes(candidate) ? candidate : null;
|
|
366
|
+
return candidate && !HEADER_EXCLUSIONS.has(candidate) ? candidate : null;
|
|
333
367
|
}
|
|
334
368
|
function matchFunctionHeader(line, ext) {
|
|
335
369
|
const trimmed = line.trim();
|
|
336
|
-
if (trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*"))
|
|
370
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*") || trimmed.startsWith("#")) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
337
373
|
if (ext === ".go") {
|
|
338
|
-
const match = line.match(/func\s+(?:\([^)]*\)\s*)?([a-zA-Z0-9_$]+)\s*\(/);
|
|
374
|
+
const match = line.match(/func\s+(?:\([^)]*\)\s*)?([a-zA-Z0-9_$]+)\s*(?:\[[^\]]*\])?\s*\(/);
|
|
339
375
|
return match ? match[1] : null;
|
|
340
376
|
}
|
|
341
377
|
if (ext === ".rs") {
|
|
342
|
-
const match = line.match(/(?:pub
|
|
378
|
+
const match = line.match(/(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?(?:unsafe\s+)?(?:const\s+)?fn\s+([a-zA-Z0-9_$]+)\s*(?:<[^>]*>)?\s*\(/);
|
|
343
379
|
return match ? match[1] : null;
|
|
344
380
|
}
|
|
345
381
|
return matchCStyleHeader(line);
|
|
346
382
|
}
|
|
347
|
-
function
|
|
348
|
-
if (
|
|
349
|
-
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
if (
|
|
353
|
-
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
+
function detectRustOrGoRawString(sub, ext) {
|
|
384
|
+
if (ext === ".rs") {
|
|
385
|
+
const m = sub.match(/^r(#*)"/);
|
|
386
|
+
return m ? { term: `"${m[1]}`, advance: m[0].length - 1 } : null;
|
|
387
|
+
}
|
|
388
|
+
if (ext === ".go" && sub.startsWith("`")) {
|
|
389
|
+
return { term: "`", advance: 0 };
|
|
390
|
+
}
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
function detectCsJavaOrCppRawString(sub, ext) {
|
|
394
|
+
if (ext === ".cs" || ext === ".java") {
|
|
395
|
+
const m = sub.match(/^\$*("{3,})/);
|
|
396
|
+
if (m) return { term: m[1], advance: m[0].length - 1 };
|
|
397
|
+
const v = ext === ".cs" ? sub.match(/^(@\$|\$@|@)"/) : null;
|
|
398
|
+
return v ? { term: '"', advance: v[0].length - 1 } : null;
|
|
399
|
+
}
|
|
400
|
+
const cpp = sub.match(/^R"([a-zA-Z0-9_]*)\(/);
|
|
401
|
+
return cpp ? { term: `)${cpp[1]}"`, advance: cpp[0].length - 1 } : null;
|
|
402
|
+
}
|
|
403
|
+
function detectRawStringOpen(line, index, ext) {
|
|
404
|
+
const sub = line.slice(index);
|
|
405
|
+
return detectRustOrGoRawString(sub, ext) || detectCsJavaOrCppRawString(sub, ext);
|
|
406
|
+
}
|
|
407
|
+
function consumeRawOrBlock(line, index, state) {
|
|
408
|
+
if (state.inBlock) {
|
|
409
|
+
if (line[index] === "*" && line[index + 1] === "/") {
|
|
410
|
+
state.inBlock = false;
|
|
411
|
+
return { advance: 1, handled: true };
|
|
412
|
+
}
|
|
413
|
+
return { advance: 0, handled: true };
|
|
414
|
+
}
|
|
415
|
+
if (state.rawTerm) {
|
|
416
|
+
if (line.startsWith(state.rawTerm, index)) {
|
|
417
|
+
const len = state.rawTerm.length - 1;
|
|
418
|
+
state.rawTerm = null;
|
|
419
|
+
return { advance: len, handled: true };
|
|
420
|
+
}
|
|
421
|
+
return { advance: 0, handled: true };
|
|
422
|
+
}
|
|
423
|
+
return { advance: 0, handled: false };
|
|
424
|
+
}
|
|
425
|
+
function consumeNormalQuote(line, index, state) {
|
|
426
|
+
if (!state.inString) return { advance: 0, handled: false };
|
|
427
|
+
const ch = line[index];
|
|
428
|
+
if (ch === "\\") return { advance: 1, handled: true };
|
|
429
|
+
if (ch === state.inString) state.inString = null;
|
|
430
|
+
return { advance: 0, handled: true };
|
|
431
|
+
}
|
|
432
|
+
function applyQuoteOrBrace(ch, state) {
|
|
433
|
+
if (ch === '"' || ch === "'") {
|
|
434
|
+
state.inString = ch;
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (ch === "{") state.delta++;
|
|
438
|
+
if (ch === "}") state.delta--;
|
|
439
|
+
}
|
|
440
|
+
function stepActiveCodeToken(line, j, ext, state) {
|
|
441
|
+
const pair = line.slice(j, j + 2);
|
|
442
|
+
if (pair === "//") return { advance: 0, stop: true };
|
|
443
|
+
if (pair === "/*") {
|
|
383
444
|
state.inBlock = true;
|
|
384
445
|
return { advance: 1, stop: false };
|
|
385
446
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
447
|
+
const rawOpen = detectRawStringOpen(line, j, ext);
|
|
448
|
+
if (rawOpen) {
|
|
449
|
+
state.rawTerm = rawOpen.term;
|
|
450
|
+
return { advance: rawOpen.advance, stop: false };
|
|
389
451
|
}
|
|
390
|
-
state
|
|
452
|
+
applyQuoteOrBrace(line[j], state);
|
|
391
453
|
return { advance: 0, stop: false };
|
|
392
454
|
}
|
|
393
|
-
function scanBracesInLine(line, wasInBlock, ext) {
|
|
394
|
-
const state = { delta: 0, inBlock: wasInBlock, inString: null };
|
|
455
|
+
function scanBracesInLine(line, wasInBlock, wasRawTerm, ext) {
|
|
456
|
+
const state = { delta: 0, inBlock: wasInBlock, rawTerm: wasRawTerm, inString: null };
|
|
395
457
|
for (let j = 0; j < line.length; j++) {
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
458
|
+
const rawOrBlock = consumeRawOrBlock(line, j, state);
|
|
459
|
+
if (rawOrBlock.handled) {
|
|
460
|
+
j += rawOrBlock.advance;
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
const quoteStep = consumeNormalQuote(line, j, state);
|
|
464
|
+
if (quoteStep.handled) {
|
|
465
|
+
j += quoteStep.advance;
|
|
403
466
|
continue;
|
|
404
467
|
}
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
j += step.advance;
|
|
468
|
+
const codeStep = stepActiveCodeToken(line, j, ext, state);
|
|
469
|
+
if (codeStep.stop) break;
|
|
470
|
+
j += codeStep.advance;
|
|
409
471
|
}
|
|
410
|
-
return { delta: state.delta, nowInBlock: state.inBlock };
|
|
472
|
+
return { delta: state.delta, nowInBlock: state.inBlock, nowRawTerm: state.rawTerm };
|
|
411
473
|
}
|
|
412
474
|
function buildMetricsFromBlocks(blocks, content, filePath) {
|
|
413
475
|
if (blocks.length === 0) {
|
|
@@ -431,11 +493,20 @@ function buildMetricsFromBlocks(blocks, content, filePath) {
|
|
|
431
493
|
}
|
|
432
494
|
function detectPolyglotSmells(body) {
|
|
433
495
|
const smells = [];
|
|
434
|
-
if (/#\s*noqa|#\s*type:\s*ignore|@SuppressWarnings/.test(body)) {
|
|
496
|
+
if (/#\s*noqa|#\s*type:\s*ignore|@SuppressWarnings|#pragma\s+warning\s+disable/.test(body)) {
|
|
435
497
|
smells.push("Supresi\xF3n no autorizada de linter");
|
|
436
498
|
}
|
|
437
499
|
return smells;
|
|
438
500
|
}
|
|
501
|
+
function stripStringsAndComments(body, ext) {
|
|
502
|
+
let cleaned = body;
|
|
503
|
+
if (ext === ".py") {
|
|
504
|
+
cleaned = cleaned.replace(/"""[\s\S]*?"""|'''[\s\S]*?'''/g, '""').replace(/#.*/g, "");
|
|
505
|
+
} else {
|
|
506
|
+
cleaned = cleaned.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, "").replace(/r(#+)"[\s\S]*?"\1/g, '""').replace(/R"([a-zA-Z0-9_]*)\([\s\S]*?\)\1"/g, '""').replace(/\$*"{3,}[\s\S]*?"{3,}/g, '""').replace(/`[\s\S]*?`/g, '""');
|
|
507
|
+
}
|
|
508
|
+
return cleaned.replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g, '""');
|
|
509
|
+
}
|
|
439
510
|
function computeHalsteadMI(body, cyclomatic, loc, commentCount) {
|
|
440
511
|
const cleanText = body.replace(/\/\*[\s\S]*?\*\/|\/\/.*|#.*/g, "");
|
|
441
512
|
const tokens = cleanText.split(/[\s,;().{}[\]=+\-*/<>!&|:]+/).filter((t) => t.length > 0);
|
|
@@ -460,9 +531,11 @@ function countCommentsAndLoc(rawLines, isPy) {
|
|
|
460
531
|
function calculatePolyglotMetrics(body, name, filePath, startLine) {
|
|
461
532
|
const ext = path4__namespace.extname(filePath).toLowerCase();
|
|
462
533
|
const rawLines = body.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
463
|
-
const { loc, commentCount
|
|
464
|
-
const
|
|
465
|
-
const
|
|
534
|
+
const { loc, commentCount } = countCommentsAndLoc(rawLines, ext === ".py");
|
|
535
|
+
const strippedBody = stripStringsAndComments(body, ext);
|
|
536
|
+
const strippedLines = strippedBody.split(/\r?\n/).filter((l) => l.trim().length > 0);
|
|
537
|
+
const cyclomatic = computePolyglotCyclomatic(strippedBody, ext);
|
|
538
|
+
const cognitive = computePolyglotCognitive(strippedLines, ext === ".py");
|
|
466
539
|
const maintainability = computeHalsteadMI(body, cyclomatic, loc, commentCount);
|
|
467
540
|
return {
|
|
468
541
|
functionName: name,
|
|
@@ -475,40 +548,60 @@ function calculatePolyglotMetrics(body, name, filePath, startLine) {
|
|
|
475
548
|
line: startLine ? startLine + 1 : 1
|
|
476
549
|
};
|
|
477
550
|
}
|
|
478
|
-
function
|
|
479
|
-
let regex;
|
|
551
|
+
function getCyclomaticPattern(ext) {
|
|
480
552
|
if (ext === ".py") {
|
|
481
|
-
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
553
|
+
return /\b(if|elif|for|while|except|case)\b|\b(and|or)\b/g;
|
|
554
|
+
}
|
|
555
|
+
if (ext === ".go") {
|
|
556
|
+
return /\b(if|for|case|select)\b|&&|\|\|/g;
|
|
557
|
+
}
|
|
558
|
+
if (ext === ".rs") {
|
|
559
|
+
return /\b(if|for|while|match)\b|&&|\|\||\?/g;
|
|
560
|
+
}
|
|
561
|
+
if (ext === ".cs") {
|
|
562
|
+
return /\b(if|else\s+if|for|foreach|while|catch|case|when)\b|\?\?|\?\.(?=[a-zA-Z_])|&&|\|\|/g;
|
|
563
|
+
}
|
|
564
|
+
if (ext === ".java") {
|
|
565
|
+
return /\b(if|else\s+if|for|while|catch|case|when)\b|&&|\|\|/g;
|
|
488
566
|
}
|
|
489
|
-
|
|
490
|
-
|
|
567
|
+
return /\b(if|else\s+if|for|while|catch|case)\b|\?|&&|\|\|/g;
|
|
568
|
+
}
|
|
569
|
+
function computePolyglotCyclomatic(strippedBody, ext) {
|
|
570
|
+
const regex = getCyclomaticPattern(ext);
|
|
571
|
+
const matches = strippedBody.match(regex);
|
|
491
572
|
return 1 + (matches ? matches.length : 0);
|
|
492
573
|
}
|
|
493
|
-
|
|
574
|
+
var PY_COGNITIVE_RE = /\b(if|elif|for|while|except|match)\b/;
|
|
575
|
+
var BRACE_COGNITIVE_RE = /\b(if|for|foreach|while|catch|match|select|switch)\b/;
|
|
576
|
+
function computePythonCognitive(lines) {
|
|
577
|
+
let cognitive = 0;
|
|
578
|
+
const baseIndent = lines.length > 0 ? lines[0].length - lines[0].trimStart().length : 0;
|
|
579
|
+
for (let i = 1; i < lines.length; i++) {
|
|
580
|
+
const line = lines[i];
|
|
581
|
+
if (PY_COGNITIVE_RE.test(line)) {
|
|
582
|
+
const relIndent = Math.max(0, line.length - line.trimStart().length - baseIndent - 4);
|
|
583
|
+
cognitive += 1 + Math.floor(relIndent / 4);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return cognitive;
|
|
587
|
+
}
|
|
588
|
+
function computeBraceCognitive(lines) {
|
|
494
589
|
let cognitive = 0;
|
|
495
590
|
let nesting = 0;
|
|
496
|
-
for (
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
cognitive += 1 + Math.floor(indent / 4);
|
|
501
|
-
}
|
|
502
|
-
} else {
|
|
503
|
-
if (line.includes("{")) nesting++;
|
|
504
|
-
if (line.includes("}")) nesting = Math.max(0, nesting - 1);
|
|
505
|
-
if (/\b(if|for|while|catch|match|select)\b/.test(line)) {
|
|
506
|
-
cognitive += 1 + nesting;
|
|
507
|
-
}
|
|
591
|
+
for (let i = 1; i < lines.length; i++) {
|
|
592
|
+
const line = lines[i];
|
|
593
|
+
if (BRACE_COGNITIVE_RE.test(line)) {
|
|
594
|
+
cognitive += 1 + nesting;
|
|
508
595
|
}
|
|
596
|
+
const opens = (line.match(/\{/g) || []).length;
|
|
597
|
+
const closes = (line.match(/\}/g) || []).length;
|
|
598
|
+
nesting = Math.max(0, nesting + opens - closes);
|
|
509
599
|
}
|
|
510
600
|
return cognitive;
|
|
511
601
|
}
|
|
602
|
+
function computePolyglotCognitive(lines, isPy) {
|
|
603
|
+
return isPy ? computePythonCognitive(lines) : computeBraceCognitive(lines);
|
|
604
|
+
}
|
|
512
605
|
function walkMdFiles(dir, fileList = [], excludeDirs = [
|
|
513
606
|
"node_modules",
|
|
514
607
|
".git",
|
|
@@ -1092,9 +1185,54 @@ function collectTestCorpus(rootDir, allMdFiles) {
|
|
|
1092
1185
|
}
|
|
1093
1186
|
return { gherkinSources, combinedTests: testContents.join("\n") };
|
|
1094
1187
|
}
|
|
1095
|
-
function
|
|
1188
|
+
function isCucumberScenarioPassed(element) {
|
|
1189
|
+
const steps = Array.isArray(element.steps) ? element.steps : [];
|
|
1190
|
+
if (steps.length === 0) return false;
|
|
1191
|
+
return steps.every((s) => {
|
|
1192
|
+
const res = s.result;
|
|
1193
|
+
return res && res.status === "passed";
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
function countCucumberScenarios(raw) {
|
|
1197
|
+
let passed = 0;
|
|
1198
|
+
let failed = 0;
|
|
1199
|
+
for (const feat of raw) {
|
|
1200
|
+
const elements = Array.isArray(feat.elements) ? feat.elements : [];
|
|
1201
|
+
for (const el of elements) {
|
|
1202
|
+
if (isCucumberScenarioPassed(el)) passed++;
|
|
1203
|
+
else failed++;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
return { passed, failed };
|
|
1207
|
+
}
|
|
1208
|
+
function parseCucumberReportFile(reportPath) {
|
|
1209
|
+
if (!fs7__namespace.existsSync(reportPath)) return void 0;
|
|
1210
|
+
try {
|
|
1211
|
+
const raw = JSON.parse(fs7__namespace.readFileSync(reportPath, "utf-8"));
|
|
1212
|
+
if (!Array.isArray(raw)) return void 0;
|
|
1213
|
+
const { passed, failed } = countCucumberScenarios(raw);
|
|
1214
|
+
return {
|
|
1215
|
+
evaluatedFeatures: raw.length,
|
|
1216
|
+
passedScenarios: passed,
|
|
1217
|
+
failedScenarios: failed,
|
|
1218
|
+
passed: passed + failed > 0 && failed === 0
|
|
1219
|
+
};
|
|
1220
|
+
} catch {
|
|
1221
|
+
return void 0;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
function resolveCucumberReport(rootDir, options) {
|
|
1225
|
+
const candidate = options.cucumberReportPath ? path4__namespace.isAbsolute(options.cucumberReportPath) ? options.cucumberReportPath : path4__namespace.join(rootDir, options.cucumberReportPath) : path4__namespace.join(rootDir, "reports", "cucumber-report.json");
|
|
1226
|
+
const parsed = parseCucumberReportFile(candidate);
|
|
1227
|
+
if (parsed) return parsed;
|
|
1228
|
+
if (options.enforceCucumberExecution) {
|
|
1229
|
+
return { evaluatedFeatures: 0, passedScenarios: 0, failedScenarios: 1, passed: false };
|
|
1230
|
+
}
|
|
1231
|
+
return void 0;
|
|
1232
|
+
}
|
|
1233
|
+
function assembleCoverageResult(reqAudit, taskAudit, useCaseFlows, reqErrors, taskErrors, ucErrors, rootDir, options) {
|
|
1096
1234
|
return {
|
|
1097
|
-
success: reqErrors === 0 && taskErrors === 0 && ucErrors === 0,
|
|
1235
|
+
success: reqErrors === 0 && taskErrors === 0 && ucErrors === 0 && (!resolveCucumberReport(rootDir, options) || resolveCucumberReport(rootDir, options).passed),
|
|
1098
1236
|
totalRequirements: reqAudit.length,
|
|
1099
1237
|
passedRequirements: reqAudit.length - reqErrors,
|
|
1100
1238
|
failedRequirements: reqErrors,
|
|
@@ -1105,6 +1243,7 @@ function assembleCoverageResult(reqAudit, taskAudit, useCaseFlows, reqErrors, ta
|
|
|
1105
1243
|
tasks: taskAudit,
|
|
1106
1244
|
useCaseFlows,
|
|
1107
1245
|
failedUseCaseFlows: ucErrors,
|
|
1246
|
+
...resolveCucumberReport(rootDir, options) ? { cucumberExecution: resolveCucumberReport(rootDir, options) } : {},
|
|
1108
1247
|
reportMarkdown: generateTestingCoverageReportMarkdown(reqAudit, taskAudit, reqErrors, taskErrors)
|
|
1109
1248
|
};
|
|
1110
1249
|
}
|
|
@@ -1156,7 +1295,9 @@ function verifyTestingCoverage(options = {}) {
|
|
|
1156
1295
|
state.useCaseFlows,
|
|
1157
1296
|
state.reqErrors,
|
|
1158
1297
|
state.taskErrors,
|
|
1159
|
-
state.ucErrors
|
|
1298
|
+
state.ucErrors,
|
|
1299
|
+
rootDir,
|
|
1300
|
+
options
|
|
1160
1301
|
);
|
|
1161
1302
|
}
|
|
1162
1303
|
|
|
@@ -5532,6 +5673,277 @@ function verifyTasksGovernance(options = {}) {
|
|
|
5532
5673
|
);
|
|
5533
5674
|
return assembleGovernanceResult(data, reportMarkdown);
|
|
5534
5675
|
}
|
|
5676
|
+
var BddStepRegistry = class {
|
|
5677
|
+
definitions = [];
|
|
5678
|
+
register(pattern, handler) {
|
|
5679
|
+
this.definitions.push({ pattern, handler });
|
|
5680
|
+
return this;
|
|
5681
|
+
}
|
|
5682
|
+
given(pattern, handler) {
|
|
5683
|
+
return this.register(pattern, handler);
|
|
5684
|
+
}
|
|
5685
|
+
when(pattern, handler) {
|
|
5686
|
+
return this.register(pattern, handler);
|
|
5687
|
+
}
|
|
5688
|
+
then(pattern, handler) {
|
|
5689
|
+
return this.register(pattern, handler);
|
|
5690
|
+
}
|
|
5691
|
+
getDefinitions() {
|
|
5692
|
+
return this.definitions;
|
|
5693
|
+
}
|
|
5694
|
+
};
|
|
5695
|
+
var STEP_PREFIX_RE = /^(Given|When|Then|And|But|Dado|Cuando|Entonces|Y|Pero)\s+(.+)$/i;
|
|
5696
|
+
var SCENARIO_HEADER_RE = /^(Scenario|Escenario|Scenario Outline|Esquema del escenario):\s*(.+)$/i;
|
|
5697
|
+
function parseTableRowCells(line) {
|
|
5698
|
+
return line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
|
|
5699
|
+
}
|
|
5700
|
+
function attachStepTableRow(lastStep, cells) {
|
|
5701
|
+
if (!lastStep || cells.length < 2) return;
|
|
5702
|
+
if (!lastStep.table) lastStep.table = {};
|
|
5703
|
+
lastStep.table[cells[0]] = cells[1];
|
|
5704
|
+
}
|
|
5705
|
+
function attachExampleRow(scenario, headers, cells) {
|
|
5706
|
+
if (!scenario) return headers;
|
|
5707
|
+
if (headers.length === 0) return cells;
|
|
5708
|
+
const row = {};
|
|
5709
|
+
for (let i = 0; i < headers.length; i++) {
|
|
5710
|
+
row[headers[i]] = cells[i] ?? "";
|
|
5711
|
+
}
|
|
5712
|
+
scenario.examples.push(row);
|
|
5713
|
+
return headers;
|
|
5714
|
+
}
|
|
5715
|
+
function createInitialParseState() {
|
|
5716
|
+
return {
|
|
5717
|
+
featureName: "Unnamed Feature",
|
|
5718
|
+
featureTags: [],
|
|
5719
|
+
pendingTags: [],
|
|
5720
|
+
backgroundSteps: [],
|
|
5721
|
+
scenarios: [],
|
|
5722
|
+
currentScenario: null,
|
|
5723
|
+
inBackground: false,
|
|
5724
|
+
inExamples: false,
|
|
5725
|
+
exampleHeaders: []
|
|
5726
|
+
};
|
|
5727
|
+
}
|
|
5728
|
+
function handleStructuralHeaderLine(line, state) {
|
|
5729
|
+
if (/^(Feature|Caracter[ií]stica):/i.test(line)) {
|
|
5730
|
+
state.featureName = line.replace(/^(Feature|Caracter[ií]stica):\s*/i, "").trim();
|
|
5731
|
+
state.featureTags.push(...state.pendingTags);
|
|
5732
|
+
state.pendingTags = [];
|
|
5733
|
+
return true;
|
|
5734
|
+
}
|
|
5735
|
+
if (/^(Background|Antecedentes):/i.test(line)) {
|
|
5736
|
+
state.inBackground = true;
|
|
5737
|
+
state.inExamples = false;
|
|
5738
|
+
state.currentScenario = null;
|
|
5739
|
+
return true;
|
|
5740
|
+
}
|
|
5741
|
+
if (/^(Examples|Ejemplos):/i.test(line)) {
|
|
5742
|
+
state.inExamples = true;
|
|
5743
|
+
state.exampleHeaders = [];
|
|
5744
|
+
return true;
|
|
5745
|
+
}
|
|
5746
|
+
return false;
|
|
5747
|
+
}
|
|
5748
|
+
function startScenarioBlock(scenMatch, state) {
|
|
5749
|
+
state.inBackground = false;
|
|
5750
|
+
state.inExamples = false;
|
|
5751
|
+
const block = {
|
|
5752
|
+
name: scenMatch[2].trim(),
|
|
5753
|
+
tags: [...state.featureTags, ...state.pendingTags],
|
|
5754
|
+
isOutline: /outline|esquema/i.test(scenMatch[1]),
|
|
5755
|
+
steps: [],
|
|
5756
|
+
examples: []
|
|
5757
|
+
};
|
|
5758
|
+
state.pendingTags = [];
|
|
5759
|
+
state.scenarios.push(block);
|
|
5760
|
+
state.currentScenario = block;
|
|
5761
|
+
}
|
|
5762
|
+
function appendParsedStep(stepMatch, state) {
|
|
5763
|
+
const step = { keyword: stepMatch[1], text: stepMatch[2].trim() };
|
|
5764
|
+
if (state.inBackground) {
|
|
5765
|
+
state.backgroundSteps.push(step);
|
|
5766
|
+
} else if (state.currentScenario) {
|
|
5767
|
+
state.currentScenario.steps.push(step);
|
|
5768
|
+
}
|
|
5769
|
+
}
|
|
5770
|
+
function handleScenarioOrStepLine(line, state) {
|
|
5771
|
+
const scenMatch = line.match(SCENARIO_HEADER_RE);
|
|
5772
|
+
if (scenMatch) {
|
|
5773
|
+
startScenarioBlock(scenMatch, state);
|
|
5774
|
+
return;
|
|
5775
|
+
}
|
|
5776
|
+
const stepMatch = line.match(STEP_PREFIX_RE);
|
|
5777
|
+
if (stepMatch) {
|
|
5778
|
+
appendParsedStep(stepMatch, state);
|
|
5779
|
+
}
|
|
5780
|
+
}
|
|
5781
|
+
function processFeatureLine(rawLine, state) {
|
|
5782
|
+
const line = rawLine.trim();
|
|
5783
|
+
if (!line || line.startsWith("#")) return;
|
|
5784
|
+
if (line.startsWith("@")) {
|
|
5785
|
+
const tags = line.split(/\s+/).filter((t) => t.startsWith("@"));
|
|
5786
|
+
state.pendingTags.push(...tags);
|
|
5787
|
+
return;
|
|
5788
|
+
}
|
|
5789
|
+
if (line.startsWith("|")) {
|
|
5790
|
+
const cells = parseTableRowCells(line);
|
|
5791
|
+
if (state.inExamples) {
|
|
5792
|
+
state.exampleHeaders = attachExampleRow(state.currentScenario, state.exampleHeaders, cells);
|
|
5793
|
+
} else {
|
|
5794
|
+
const targetList = state.inBackground ? state.backgroundSteps : state.currentScenario?.steps || [];
|
|
5795
|
+
attachStepTableRow(targetList[targetList.length - 1], cells);
|
|
5796
|
+
}
|
|
5797
|
+
return;
|
|
5798
|
+
}
|
|
5799
|
+
if (!handleStructuralHeaderLine(line, state)) {
|
|
5800
|
+
handleScenarioOrStepLine(line, state);
|
|
5801
|
+
}
|
|
5802
|
+
}
|
|
5803
|
+
function parseFeatureDocument(content) {
|
|
5804
|
+
const state = createInitialParseState();
|
|
5805
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
5806
|
+
processFeatureLine(rawLine, state);
|
|
5807
|
+
}
|
|
5808
|
+
return {
|
|
5809
|
+
featureName: state.featureName,
|
|
5810
|
+
tags: state.featureTags,
|
|
5811
|
+
backgroundSteps: state.backgroundSteps,
|
|
5812
|
+
scenarios: state.scenarios
|
|
5813
|
+
};
|
|
5814
|
+
}
|
|
5815
|
+
function interpolatePlaceholders(text, row) {
|
|
5816
|
+
let out = text;
|
|
5817
|
+
for (const [k, v] of Object.entries(row)) {
|
|
5818
|
+
out = out.replace(new RegExp(`<${k}>`, "g"), v);
|
|
5819
|
+
}
|
|
5820
|
+
return out;
|
|
5821
|
+
}
|
|
5822
|
+
function expandScenarioOutlines(scenarios) {
|
|
5823
|
+
const expanded = [];
|
|
5824
|
+
for (const sc of scenarios) {
|
|
5825
|
+
if (!sc.isOutline || sc.examples.length === 0) {
|
|
5826
|
+
expanded.push(sc);
|
|
5827
|
+
continue;
|
|
5828
|
+
}
|
|
5829
|
+
sc.examples.forEach((row, idx) => {
|
|
5830
|
+
expanded.push({
|
|
5831
|
+
name: `${sc.name} (Example #${idx + 1})`,
|
|
5832
|
+
tags: sc.tags,
|
|
5833
|
+
isOutline: false,
|
|
5834
|
+
steps: sc.steps.map((st) => ({
|
|
5835
|
+
keyword: st.keyword,
|
|
5836
|
+
text: interpolatePlaceholders(st.text, row),
|
|
5837
|
+
table: st.table
|
|
5838
|
+
})),
|
|
5839
|
+
examples: []
|
|
5840
|
+
});
|
|
5841
|
+
});
|
|
5842
|
+
}
|
|
5843
|
+
return expanded;
|
|
5844
|
+
}
|
|
5845
|
+
function matchStepDefinition(text, definitions) {
|
|
5846
|
+
for (const entry of definitions) {
|
|
5847
|
+
if (typeof entry.pattern === "string") {
|
|
5848
|
+
if (entry.pattern === text) return { entry, args: [] };
|
|
5849
|
+
} else {
|
|
5850
|
+
const m = text.match(entry.pattern);
|
|
5851
|
+
if (m) return { entry, args: m.slice(1) };
|
|
5852
|
+
}
|
|
5853
|
+
}
|
|
5854
|
+
return null;
|
|
5855
|
+
}
|
|
5856
|
+
async function executeSingleStep(step, definitions) {
|
|
5857
|
+
const matched = matchStepDefinition(step.text, definitions);
|
|
5858
|
+
if (!matched) {
|
|
5859
|
+
return { keyword: step.keyword, text: step.text, status: "undefined", durationMs: 0 };
|
|
5860
|
+
}
|
|
5861
|
+
const start = Date.now();
|
|
5862
|
+
try {
|
|
5863
|
+
await matched.entry.handler(matched.args, step.table);
|
|
5864
|
+
return { keyword: step.keyword, text: step.text, status: "passed", durationMs: Date.now() - start };
|
|
5865
|
+
} catch (err) {
|
|
5866
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5867
|
+
return {
|
|
5868
|
+
keyword: step.keyword,
|
|
5869
|
+
text: step.text,
|
|
5870
|
+
status: "failed",
|
|
5871
|
+
durationMs: Date.now() - start,
|
|
5872
|
+
errorMessage: msg
|
|
5873
|
+
};
|
|
5874
|
+
}
|
|
5875
|
+
}
|
|
5876
|
+
async function executeScenarioSteps(allSteps, definitions) {
|
|
5877
|
+
const stepResults = [];
|
|
5878
|
+
let status = "passed";
|
|
5879
|
+
for (const step of allSteps) {
|
|
5880
|
+
if (status !== "passed") {
|
|
5881
|
+
stepResults.push({ keyword: step.keyword, text: step.text, status: "skipped", durationMs: 0 });
|
|
5882
|
+
continue;
|
|
5883
|
+
}
|
|
5884
|
+
const res = await executeSingleStep(step, definitions);
|
|
5885
|
+
stepResults.push(res);
|
|
5886
|
+
if (res.status === "failed" || res.status === "undefined") {
|
|
5887
|
+
status = res.status;
|
|
5888
|
+
}
|
|
5889
|
+
}
|
|
5890
|
+
return { stepResults, status };
|
|
5891
|
+
}
|
|
5892
|
+
async function runAllExpandedScenarios(parsed, definitions) {
|
|
5893
|
+
const expanded = expandScenarioOutlines(parsed.scenarios);
|
|
5894
|
+
const out = [];
|
|
5895
|
+
for (const sc of expanded) {
|
|
5896
|
+
const combinedSteps = [...parsed.backgroundSteps, ...sc.steps];
|
|
5897
|
+
const { stepResults, status } = await executeScenarioSteps(combinedSteps, definitions);
|
|
5898
|
+
out.push({ name: sc.name, tags: sc.tags, status, steps: stepResults });
|
|
5899
|
+
}
|
|
5900
|
+
return out;
|
|
5901
|
+
}
|
|
5902
|
+
async function executeBddFeature(featurePath, registry, options = {}) {
|
|
5903
|
+
const rootDir = options.rootDir || process.cwd();
|
|
5904
|
+
const fullPath = path4__namespace.isAbsolute(featurePath) ? featurePath : path4__namespace.join(rootDir, featurePath);
|
|
5905
|
+
const parsed = parseFeatureDocument(fs7__namespace.readFileSync(fullPath, "utf-8"));
|
|
5906
|
+
const scenarios = await runAllExpandedScenarios(parsed, registry.getDefinitions());
|
|
5907
|
+
const report = {
|
|
5908
|
+
uri: path4__namespace.relative(rootDir, fullPath).replace(/\\/g, "/"),
|
|
5909
|
+
featureName: parsed.featureName,
|
|
5910
|
+
tags: parsed.tags,
|
|
5911
|
+
scenarios,
|
|
5912
|
+
passed: scenarios.length > 0 && scenarios.every((s) => s.status === "passed")
|
|
5913
|
+
};
|
|
5914
|
+
if (options.writeReportPath) writeCucumberJsonReport([report], options.writeReportPath);
|
|
5915
|
+
return report;
|
|
5916
|
+
}
|
|
5917
|
+
function mapStepToCucumberJson(st) {
|
|
5918
|
+
return {
|
|
5919
|
+
keyword: `${st.keyword} `,
|
|
5920
|
+
name: st.text,
|
|
5921
|
+
result: {
|
|
5922
|
+
status: st.status,
|
|
5923
|
+
duration: st.durationMs * 1e6,
|
|
5924
|
+
...st.errorMessage ? { error_message: st.errorMessage } : {}
|
|
5925
|
+
}
|
|
5926
|
+
};
|
|
5927
|
+
}
|
|
5928
|
+
function mapFeatureToCucumberJson(r) {
|
|
5929
|
+
return {
|
|
5930
|
+
uri: r.uri,
|
|
5931
|
+
name: r.featureName,
|
|
5932
|
+
tags: r.tags.map((t) => ({ name: t })),
|
|
5933
|
+
elements: r.scenarios.map((sc) => ({
|
|
5934
|
+
type: "scenario",
|
|
5935
|
+
name: sc.name,
|
|
5936
|
+
tags: sc.tags.map((t) => ({ name: t })),
|
|
5937
|
+
steps: sc.steps.map(mapStepToCucumberJson)
|
|
5938
|
+
}))
|
|
5939
|
+
};
|
|
5940
|
+
}
|
|
5941
|
+
function writeCucumberJsonReport(reports, outputPath) {
|
|
5942
|
+
const dir = path4__namespace.dirname(outputPath);
|
|
5943
|
+
if (!fs7__namespace.existsSync(dir)) fs7__namespace.mkdirSync(dir, { recursive: true });
|
|
5944
|
+
const payload = JSON.stringify(reports.map(mapFeatureToCucumberJson), null, 2);
|
|
5945
|
+
fs7__namespace.writeFileSync(outputPath, payload, "utf-8");
|
|
5946
|
+
}
|
|
5535
5947
|
function isToolAvailable(toolName) {
|
|
5536
5948
|
try {
|
|
5537
5949
|
const cmd = process.platform === "win32" ? "where" : "which";
|
|
@@ -5685,6 +6097,214 @@ function scanPnpmStore(pnpmDir, directNames, seen) {
|
|
|
5685
6097
|
}
|
|
5686
6098
|
}
|
|
5687
6099
|
}
|
|
6100
|
+
function readTextIfExists(filePath) {
|
|
6101
|
+
if (!fs7__namespace.existsSync(filePath)) return null;
|
|
6102
|
+
try {
|
|
6103
|
+
return fs7__namespace.readFileSync(filePath, "utf-8");
|
|
6104
|
+
} catch {
|
|
6105
|
+
return null;
|
|
6106
|
+
}
|
|
6107
|
+
}
|
|
6108
|
+
function extractTomlField(block, key) {
|
|
6109
|
+
const m = block.match(new RegExp(`^\\s*${key}\\s*=\\s*["']([^"']+)["']`, "m"));
|
|
6110
|
+
return m ? m[1].trim() : void 0;
|
|
6111
|
+
}
|
|
6112
|
+
function extractPyprojectDirectDeps(content) {
|
|
6113
|
+
const direct = /* @__PURE__ */ new Set();
|
|
6114
|
+
const matches = content.matchAll(/^([a-zA-Z0-9_.-]+)\s*=\s*(?:["'{]|true)/gm);
|
|
6115
|
+
for (const m of matches) {
|
|
6116
|
+
const key = m[1].toLowerCase();
|
|
6117
|
+
if (!["name", "version", "description", "license", "python", "readme"].includes(key)) {
|
|
6118
|
+
direct.add(key);
|
|
6119
|
+
}
|
|
6120
|
+
}
|
|
6121
|
+
return direct;
|
|
6122
|
+
}
|
|
6123
|
+
function parseSingleTomlBlock(block, lockPath, directNames) {
|
|
6124
|
+
const name = extractTomlField(block, "name");
|
|
6125
|
+
const version = extractTomlField(block, "version");
|
|
6126
|
+
if (!name || !version) return null;
|
|
6127
|
+
const spdx = normalizeSpdxLicense(extractTomlField(block, "license") || "UNKNOWN");
|
|
6128
|
+
return {
|
|
6129
|
+
name,
|
|
6130
|
+
version,
|
|
6131
|
+
spdxLicense: spdx,
|
|
6132
|
+
purl: `pkg:pypi/${name.toLowerCase()}@${version}`,
|
|
6133
|
+
path: lockPath,
|
|
6134
|
+
isDirect: directNames.size === 0 || directNames.has(name.toLowerCase())
|
|
6135
|
+
};
|
|
6136
|
+
}
|
|
6137
|
+
function parseTomlPackageBlocks(content, lockPath, directNames, seen) {
|
|
6138
|
+
for (const block of content.split(/\[\[package\]\]/g).slice(1)) {
|
|
6139
|
+
const dep = parseSingleTomlBlock(block, lockPath, directNames);
|
|
6140
|
+
if (dep && !seen.has(`pypi:${dep.name}@${dep.version}`)) {
|
|
6141
|
+
seen.set(`pypi:${dep.name}@${dep.version}`, dep);
|
|
6142
|
+
}
|
|
6143
|
+
}
|
|
6144
|
+
}
|
|
6145
|
+
function parsePipfileSection(pkgs, isDirect, lockPath, seen) {
|
|
6146
|
+
for (const [name, meta] of Object.entries(pkgs)) {
|
|
6147
|
+
const rawVer = String(meta.version || "").replace(/^==/, "") || "unknown";
|
|
6148
|
+
const spdx = normalizeSpdxLicense(meta.license || "UNKNOWN");
|
|
6149
|
+
const key = `pypi:${name}@${rawVer}`;
|
|
6150
|
+
if (!seen.has(key)) {
|
|
6151
|
+
seen.set(key, {
|
|
6152
|
+
name,
|
|
6153
|
+
version: rawVer,
|
|
6154
|
+
spdxLicense: spdx,
|
|
6155
|
+
purl: `pkg:pypi/${name.toLowerCase()}@${rawVer}`,
|
|
6156
|
+
path: lockPath,
|
|
6157
|
+
isDirect
|
|
6158
|
+
});
|
|
6159
|
+
}
|
|
6160
|
+
}
|
|
6161
|
+
}
|
|
6162
|
+
function scanPythonDependencies(rootDir, seen) {
|
|
6163
|
+
const pyproject = readTextIfExists(path4__namespace.join(rootDir, "pyproject.toml")) || "";
|
|
6164
|
+
const directNames = extractPyprojectDirectDeps(pyproject);
|
|
6165
|
+
for (const lockName of ["poetry.lock", "uv.lock"]) {
|
|
6166
|
+
const lockPath = path4__namespace.join(rootDir, lockName);
|
|
6167
|
+
const content = readTextIfExists(lockPath);
|
|
6168
|
+
if (content) parseTomlPackageBlocks(content, lockPath, directNames, seen);
|
|
6169
|
+
}
|
|
6170
|
+
const pipfileLockPath = path4__namespace.join(rootDir, "Pipfile.lock");
|
|
6171
|
+
const pipfileRaw = readTextIfExists(pipfileLockPath);
|
|
6172
|
+
if (!pipfileRaw) return;
|
|
6173
|
+
try {
|
|
6174
|
+
const data = JSON.parse(pipfileRaw);
|
|
6175
|
+
parsePipfileSection(data.default || {}, true, pipfileLockPath, seen);
|
|
6176
|
+
parsePipfileSection(data.develop || {}, false, pipfileLockPath, seen);
|
|
6177
|
+
} catch {
|
|
6178
|
+
}
|
|
6179
|
+
}
|
|
6180
|
+
function parseGoRequireLine(line, goModPath) {
|
|
6181
|
+
const cleaned = line.replace(/^require\s+/, "").trim();
|
|
6182
|
+
const commentIdx = cleaned.indexOf("//");
|
|
6183
|
+
const codePart = (commentIdx >= 0 ? cleaned.slice(0, commentIdx) : cleaned).trim();
|
|
6184
|
+
const commentPart = commentIdx >= 0 ? cleaned.slice(commentIdx + 2).trim() : "";
|
|
6185
|
+
const parts = codePart.split(/\s+/);
|
|
6186
|
+
if (parts.length < 2) return null;
|
|
6187
|
+
const modName = parts[0];
|
|
6188
|
+
const rawVer = parts[1].replace(/^v/, "");
|
|
6189
|
+
const isIndirect = /\bindirect\b/i.test(commentPart);
|
|
6190
|
+
const licenseMatch = commentPart.match(/(?:spdx|license)\s*:\s*([A-Za-z0-9.+-]+)/i);
|
|
6191
|
+
const spdx = licenseMatch ? normalizeSpdxLicense(licenseMatch[1]) : "UNKNOWN";
|
|
6192
|
+
return {
|
|
6193
|
+
name: modName,
|
|
6194
|
+
version: rawVer,
|
|
6195
|
+
spdxLicense: spdx,
|
|
6196
|
+
purl: `pkg:golang/${modName}@v${rawVer}`,
|
|
6197
|
+
path: goModPath,
|
|
6198
|
+
isDirect: !isIndirect
|
|
6199
|
+
};
|
|
6200
|
+
}
|
|
6201
|
+
function extractGoCandidateLines(content) {
|
|
6202
|
+
const out = [];
|
|
6203
|
+
let inBlock = false;
|
|
6204
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
6205
|
+
const trimmed = rawLine.trim();
|
|
6206
|
+
if (/^require\s*\($/.test(trimmed)) inBlock = true;
|
|
6207
|
+
else if (inBlock && trimmed === ")") inBlock = false;
|
|
6208
|
+
else if (inBlock || trimmed.startsWith("require ")) out.push(trimmed);
|
|
6209
|
+
}
|
|
6210
|
+
return out;
|
|
6211
|
+
}
|
|
6212
|
+
function scanGoDependencies(rootDir, seen) {
|
|
6213
|
+
const goModPath = path4__namespace.join(rootDir, "go.mod");
|
|
6214
|
+
const content = readTextIfExists(goModPath);
|
|
6215
|
+
if (!content) return;
|
|
6216
|
+
for (const line of extractGoCandidateLines(content)) {
|
|
6217
|
+
const dep = parseGoRequireLine(line, goModPath);
|
|
6218
|
+
if (dep && !seen.has(`golang:${dep.name}@${dep.version}`)) {
|
|
6219
|
+
seen.set(`golang:${dep.name}@${dep.version}`, dep);
|
|
6220
|
+
}
|
|
6221
|
+
}
|
|
6222
|
+
}
|
|
6223
|
+
function extractCargoDirectDeps(cargoToml) {
|
|
6224
|
+
const map = /* @__PURE__ */ new Map();
|
|
6225
|
+
let inDeps = false;
|
|
6226
|
+
for (const line of cargoToml.split(/\r?\n/)) {
|
|
6227
|
+
const trimmed = line.trim();
|
|
6228
|
+
if (trimmed.startsWith("[")) {
|
|
6229
|
+
inDeps = /^\[(?:dev-|build-)?dependencies\]$/.test(trimmed);
|
|
6230
|
+
continue;
|
|
6231
|
+
}
|
|
6232
|
+
if (!inDeps || !trimmed || trimmed.startsWith("#")) continue;
|
|
6233
|
+
const m = trimmed.match(/^([a-zA-Z0-9_-]+)\s*=\s*(.+)$/);
|
|
6234
|
+
if (m) {
|
|
6235
|
+
const licMatch = m[2].match(/license\s*=\s*["']([^"']+)["']/);
|
|
6236
|
+
map.set(m[1], licMatch ? licMatch[1] : "UNKNOWN");
|
|
6237
|
+
}
|
|
6238
|
+
}
|
|
6239
|
+
return map;
|
|
6240
|
+
}
|
|
6241
|
+
function parseCargoLockBlock(block, rootName, directMeta, cargoLockPath) {
|
|
6242
|
+
const name = extractTomlField(block, "name");
|
|
6243
|
+
const version = extractTomlField(block, "version");
|
|
6244
|
+
if (!name || !version || name === rootName) return null;
|
|
6245
|
+
const blockLic = extractTomlField(block, "license") || directMeta.get(name) || "UNKNOWN";
|
|
6246
|
+
return {
|
|
6247
|
+
name,
|
|
6248
|
+
version,
|
|
6249
|
+
spdxLicense: normalizeSpdxLicense(blockLic),
|
|
6250
|
+
purl: `pkg:cargo/${name}@${version}`,
|
|
6251
|
+
path: cargoLockPath,
|
|
6252
|
+
isDirect: directMeta.size === 0 || directMeta.has(name)
|
|
6253
|
+
};
|
|
6254
|
+
}
|
|
6255
|
+
function scanRustDependencies(rootDir, seen) {
|
|
6256
|
+
const cargoLockPath = path4__namespace.join(rootDir, "Cargo.lock");
|
|
6257
|
+
const lockContent = readTextIfExists(cargoLockPath);
|
|
6258
|
+
if (!lockContent) return;
|
|
6259
|
+
const cargoToml = readTextIfExists(path4__namespace.join(rootDir, "Cargo.toml")) || "";
|
|
6260
|
+
const directMeta = extractCargoDirectDeps(cargoToml);
|
|
6261
|
+
const rootName = extractTomlField(cargoToml, "name");
|
|
6262
|
+
for (const block of lockContent.split(/\[\[package\]\]/g).slice(1)) {
|
|
6263
|
+
const dep = parseCargoLockBlock(block, rootName, directMeta, cargoLockPath);
|
|
6264
|
+
if (dep && !seen.has(`cargo:${dep.name}@${dep.version}`)) {
|
|
6265
|
+
seen.set(`cargo:${dep.name}@${dep.version}`, dep);
|
|
6266
|
+
}
|
|
6267
|
+
}
|
|
6268
|
+
}
|
|
6269
|
+
function extractXmlTag(xml, tag) {
|
|
6270
|
+
const m = xml.match(new RegExp(`<${tag}>([^<]+)</${tag}>`));
|
|
6271
|
+
return m ? m[1].trim() : void 0;
|
|
6272
|
+
}
|
|
6273
|
+
function resolveMavenBlockLicense(block, defaultLic) {
|
|
6274
|
+
const tagLic = extractXmlTag(block, "license");
|
|
6275
|
+
if (tagLic) return tagLic;
|
|
6276
|
+
const commentLic = block.match(/<!--\s*(?:spdx|license)\s*:\s*([A-Za-z0-9.+-]+)\s*-->/i)?.[1];
|
|
6277
|
+
return commentLic || defaultLic;
|
|
6278
|
+
}
|
|
6279
|
+
function parseMavenDepBlock(block, defaultLic, pomPath) {
|
|
6280
|
+
const groupId = extractXmlTag(block, "groupId");
|
|
6281
|
+
const artifactId = extractXmlTag(block, "artifactId");
|
|
6282
|
+
if (!groupId || !artifactId) return null;
|
|
6283
|
+
const version = extractXmlTag(block, "version") || "unknown";
|
|
6284
|
+
const inlineLic = resolveMavenBlockLicense(block, defaultLic);
|
|
6285
|
+
return {
|
|
6286
|
+
name: `${groupId}:${artifactId}`,
|
|
6287
|
+
version,
|
|
6288
|
+
spdxLicense: normalizeSpdxLicense(inlineLic),
|
|
6289
|
+
purl: `pkg:maven/${groupId}/${artifactId}@${version}`,
|
|
6290
|
+
path: pomPath,
|
|
6291
|
+
isDirect: true
|
|
6292
|
+
};
|
|
6293
|
+
}
|
|
6294
|
+
function scanMavenDependencies(rootDir, seen) {
|
|
6295
|
+
const pomPath = path4__namespace.join(rootDir, "pom.xml");
|
|
6296
|
+
const content = readTextIfExists(pomPath);
|
|
6297
|
+
if (!content) return;
|
|
6298
|
+
const projectLicBlock = content.match(/<licenses>([\s\S]*?)<\/licenses>/)?.[1] || "";
|
|
6299
|
+
const defaultLic = extractXmlTag(projectLicBlock, "name") || "UNKNOWN";
|
|
6300
|
+
const depBlocks = content.match(/<dependency>[\s\S]*?<\/dependency>/g) || [];
|
|
6301
|
+
for (const block of depBlocks) {
|
|
6302
|
+
const dep = parseMavenDepBlock(block, defaultLic, pomPath);
|
|
6303
|
+
if (dep && !seen.has(`maven:${dep.name}@${dep.version}`)) {
|
|
6304
|
+
seen.set(`maven:${dep.name}@${dep.version}`, dep);
|
|
6305
|
+
}
|
|
6306
|
+
}
|
|
6307
|
+
}
|
|
5688
6308
|
function scanWithNativeFs(rootDir, options = {}) {
|
|
5689
6309
|
const directNames = extractDirectDepNames(rootDir, options.productionOnly);
|
|
5690
6310
|
const seen = /* @__PURE__ */ new Map();
|
|
@@ -5695,6 +6315,10 @@ function scanWithNativeFs(rootDir, options = {}) {
|
|
|
5695
6315
|
scanPnpmStore(path4__namespace.join(nmDir, ".pnpm"), directNames, seen);
|
|
5696
6316
|
}
|
|
5697
6317
|
}
|
|
6318
|
+
scanPythonDependencies(rootDir, seen);
|
|
6319
|
+
scanGoDependencies(rootDir, seen);
|
|
6320
|
+
scanRustDependencies(rootDir, seen);
|
|
6321
|
+
scanMavenDependencies(rootDir, seen);
|
|
5698
6322
|
const allDeps = Array.from(seen.values());
|
|
5699
6323
|
if (options.depth === "direct") {
|
|
5700
6324
|
return allDeps.filter((d) => d.isDirect);
|
|
@@ -5764,10 +6388,17 @@ var SEVERITY_RANK = {
|
|
|
5764
6388
|
LOW: 1,
|
|
5765
6389
|
UNKNOWN: 0
|
|
5766
6390
|
};
|
|
6391
|
+
function isExemptByVex(v, vexStatements) {
|
|
6392
|
+
if (!vexStatements || vexStatements.length === 0) return false;
|
|
6393
|
+
return vexStatements.some(
|
|
6394
|
+
(stmt) => stmt.id === v.id && (stmt.state === "not_affected" || stmt.state === "fixed") && (!stmt.package || stmt.package === v.packageName)
|
|
6395
|
+
);
|
|
6396
|
+
}
|
|
5767
6397
|
function evaluateVulnerabilityThresholds(vulnerabilities, options) {
|
|
5768
6398
|
const minRank = options?.failOnSeverity ? SEVERITY_RANK[options.failOnSeverity] ?? 3 : void 0;
|
|
5769
6399
|
const maxScore = options?.maxCvssScore;
|
|
5770
6400
|
const failing = vulnerabilities.filter((v) => {
|
|
6401
|
+
if (isExemptByVex(v, options?.vexStatements)) return false;
|
|
5771
6402
|
const rankMatch = minRank !== void 0 && SEVERITY_RANK[v.severity] >= minRank;
|
|
5772
6403
|
const scoreMatch = maxScore !== void 0 && v.cvssV3Score !== void 0 && v.cvssV3Score >= maxScore;
|
|
5773
6404
|
return rankMatch || scoreMatch;
|
|
@@ -5897,42 +6528,77 @@ function buildCycloneDxComponent(dep) {
|
|
|
5897
6528
|
}
|
|
5898
6529
|
return comp;
|
|
5899
6530
|
}
|
|
5900
|
-
function
|
|
6531
|
+
function buildVexAnalysis(vex) {
|
|
6532
|
+
if (!vex) return void 0;
|
|
6533
|
+
return {
|
|
6534
|
+
state: vex.state,
|
|
6535
|
+
justification: vex.justification,
|
|
6536
|
+
detail: vex.detail,
|
|
6537
|
+
response: vex.response
|
|
6538
|
+
};
|
|
6539
|
+
}
|
|
6540
|
+
function mapScaVulnToCycloneDx(v, vexStatements) {
|
|
6541
|
+
const matchingVex = vexStatements.find(
|
|
6542
|
+
(stmt) => stmt.id === v.id && (!stmt.package || stmt.package === v.packageName)
|
|
6543
|
+
);
|
|
6544
|
+
return {
|
|
6545
|
+
id: v.id,
|
|
6546
|
+
source: v.primaryUrl ? { name: "NVD/GHSA", url: v.primaryUrl } : void 0,
|
|
6547
|
+
ratings: [{ score: v.cvssV3Score, severity: v.severity.toLowerCase(), method: "CVSSv3", vector: v.cvssV3Vector }],
|
|
6548
|
+
analysis: buildVexAnalysis(matchingVex),
|
|
6549
|
+
affects: [{ ref: `pkg:npm/${v.packageName}@${v.installedVersion}` }]
|
|
6550
|
+
};
|
|
6551
|
+
}
|
|
6552
|
+
function mapStandaloneVex(stmt) {
|
|
6553
|
+
return {
|
|
6554
|
+
id: stmt.id,
|
|
6555
|
+
analysis: buildVexAnalysis(stmt),
|
|
6556
|
+
affects: stmt.package ? [{ ref: stmt.package }] : void 0
|
|
6557
|
+
};
|
|
6558
|
+
}
|
|
6559
|
+
function buildCycloneDxVulnerabilities(vulnerabilities = [], vexStatements = []) {
|
|
6560
|
+
const out = vulnerabilities.map((v) => mapScaVulnToCycloneDx(v, vexStatements));
|
|
6561
|
+
const seenIds = new Set(out.map((item) => item.id));
|
|
6562
|
+
for (const stmt of vexStatements) {
|
|
6563
|
+
if (!seenIds.has(stmt.id)) {
|
|
6564
|
+
out.push(mapStandaloneVex(stmt));
|
|
6565
|
+
seenIds.add(stmt.id);
|
|
6566
|
+
}
|
|
6567
|
+
}
|
|
6568
|
+
return out;
|
|
6569
|
+
}
|
|
6570
|
+
function resolveProjectIdentity(options) {
|
|
5901
6571
|
let name = options.projectName || "ai-sdlc-project";
|
|
5902
6572
|
let version = options.projectVersion || "1.0.0";
|
|
5903
|
-
if (options.rootDir) {
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
}
|
|
5912
|
-
}
|
|
6573
|
+
if (!options.rootDir) return { name, version };
|
|
6574
|
+
const pkgPath = path4__namespace.join(options.rootDir, "package.json");
|
|
6575
|
+
if (!fs7__namespace.existsSync(pkgPath)) return { name, version };
|
|
6576
|
+
try {
|
|
6577
|
+
const pkg = JSON.parse(fs7__namespace.readFileSync(pkgPath, "utf-8"));
|
|
6578
|
+
if (pkg.name) name = pkg.name;
|
|
6579
|
+
if (pkg.version) version = pkg.version;
|
|
6580
|
+
} catch {
|
|
5913
6581
|
}
|
|
5914
|
-
|
|
6582
|
+
return { name, version };
|
|
6583
|
+
}
|
|
6584
|
+
function buildBomMetadata(name, version) {
|
|
6585
|
+
return {
|
|
6586
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6587
|
+
tools: [{ vendor: "AI-SDLC", name: "@aisdlc/core", version: "2.4.0" }],
|
|
6588
|
+
component: { type: "application", name, version }
|
|
6589
|
+
};
|
|
6590
|
+
}
|
|
6591
|
+
function generateCycloneDxSbom(dependencies, options = {}) {
|
|
6592
|
+
const { name, version } = resolveProjectIdentity(options);
|
|
6593
|
+
const cdxVulns = buildCycloneDxVulnerabilities(options.vulnerabilities, options.vexStatements);
|
|
5915
6594
|
return {
|
|
5916
6595
|
bomFormat: "CycloneDX",
|
|
5917
6596
|
specVersion: "1.5",
|
|
5918
6597
|
version: 1,
|
|
5919
6598
|
serialNumber: `urn:uuid:${crypto__namespace.randomUUID()}`,
|
|
5920
|
-
metadata:
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
{
|
|
5924
|
-
vendor: "AI-SDLC",
|
|
5925
|
-
name: "@aisdlc/core",
|
|
5926
|
-
version: "1.0.0"
|
|
5927
|
-
}
|
|
5928
|
-
],
|
|
5929
|
-
component: {
|
|
5930
|
-
type: "application",
|
|
5931
|
-
name,
|
|
5932
|
-
version
|
|
5933
|
-
}
|
|
5934
|
-
},
|
|
5935
|
-
components
|
|
6599
|
+
metadata: buildBomMetadata(name, version),
|
|
6600
|
+
components: dependencies.map(buildCycloneDxComponent),
|
|
6601
|
+
...cdxVulns.length > 0 ? { vulnerabilities: cdxVulns } : {}
|
|
5936
6602
|
};
|
|
5937
6603
|
}
|
|
5938
6604
|
function tryAtomicWrite(filePath, content) {
|
|
@@ -6100,17 +6766,24 @@ function parseExceptions(exceptions) {
|
|
|
6100
6766
|
}
|
|
6101
6767
|
return map;
|
|
6102
6768
|
}
|
|
6769
|
+
function parseVexStatements(doc) {
|
|
6770
|
+
const rawList = doc?.vex_statements || doc?.exceptions?.vex_statements || [];
|
|
6771
|
+
if (!Array.isArray(rawList)) return [];
|
|
6772
|
+
return rawList.filter((item) => Boolean(item && typeof item.id === "string" && typeof item.state === "string"));
|
|
6773
|
+
}
|
|
6103
6774
|
function parseLicensePolicy(content) {
|
|
6104
6775
|
try {
|
|
6105
6776
|
const doc = yaml3__default.default.load(content);
|
|
6106
6777
|
const { permitted, restricted, blocked } = classifyCategories(doc?.categories);
|
|
6107
6778
|
const exception_packages = parseExceptions(doc?.exceptions);
|
|
6779
|
+
const vex_statements = parseVexStatements(doc);
|
|
6108
6780
|
return {
|
|
6109
6781
|
version: doc?.version || "1.0",
|
|
6110
6782
|
permitted: permitted.length > 0 ? permitted : DEFAULT_PERMITTED,
|
|
6111
6783
|
restricted: restricted.length > 0 ? restricted : DEFAULT_RESTRICTED,
|
|
6112
6784
|
blocked: blocked.length > 0 ? blocked : DEFAULT_BLOCKED,
|
|
6113
6785
|
exception_packages,
|
|
6786
|
+
vex_statements,
|
|
6114
6787
|
rawPolicy: doc
|
|
6115
6788
|
};
|
|
6116
6789
|
} catch {
|
|
@@ -6119,7 +6792,8 @@ function parseLicensePolicy(content) {
|
|
|
6119
6792
|
permitted: DEFAULT_PERMITTED,
|
|
6120
6793
|
restricted: DEFAULT_RESTRICTED,
|
|
6121
6794
|
blocked: DEFAULT_BLOCKED,
|
|
6122
|
-
exception_packages: {}
|
|
6795
|
+
exception_packages: {},
|
|
6796
|
+
vex_statements: []
|
|
6123
6797
|
};
|
|
6124
6798
|
}
|
|
6125
6799
|
}
|
|
@@ -6294,10 +6968,10 @@ function shouldGenerateSbom(options, rawPolicy) {
|
|
|
6294
6968
|
if (options.generateSbom === false) return false;
|
|
6295
6969
|
return Boolean(options.generateSbom || options.sbomPath || rawPolicy?.ci_gates?.generate_sbom);
|
|
6296
6970
|
}
|
|
6297
|
-
function maybeGenerateSbom(options, rootDir, rawPolicy, dependencies) {
|
|
6971
|
+
function maybeGenerateSbom(options, rootDir, rawPolicy, dependencies, vulnerabilities, vexStatements) {
|
|
6298
6972
|
if (!shouldGenerateSbom(options, rawPolicy)) return void 0;
|
|
6299
6973
|
const sbomPath = options.sbomPath || path4__namespace.join(rootDir, "reports", "sbom.cdx.json");
|
|
6300
|
-
const sbom = generateCycloneDxSbom(dependencies, { rootDir });
|
|
6974
|
+
const sbom = generateCycloneDxSbom(dependencies, { rootDir, vulnerabilities, vexStatements });
|
|
6301
6975
|
writeCycloneDxSbom(sbom, sbomPath);
|
|
6302
6976
|
return sbomPath;
|
|
6303
6977
|
}
|
|
@@ -6310,9 +6984,9 @@ function maybeGenerateNotices(options, rootDir, rawPolicy, dependencies) {
|
|
|
6310
6984
|
writeThirdPartyNotices(noticesContent, noticesPath);
|
|
6311
6985
|
return noticesPath;
|
|
6312
6986
|
}
|
|
6313
|
-
function handleDynamicScaArtifacts(options, rootDir, rawPolicy, dependencies) {
|
|
6987
|
+
function handleDynamicScaArtifacts(options, rootDir, rawPolicy, dependencies, vulnerabilities, vexStatements) {
|
|
6314
6988
|
return {
|
|
6315
|
-
sbomPath: maybeGenerateSbom(options, rootDir, rawPolicy, dependencies),
|
|
6989
|
+
sbomPath: maybeGenerateSbom(options, rootDir, rawPolicy, dependencies, vulnerabilities, vexStatements),
|
|
6316
6990
|
noticesPath: maybeGenerateNotices(options, rootDir, rawPolicy, dependencies)
|
|
6317
6991
|
};
|
|
6318
6992
|
}
|
|
@@ -6341,7 +7015,7 @@ function buildSkippedVulnResult() {
|
|
|
6341
7015
|
skipReason: "Trivy scanner is not installed on host environment. Vulnerability check gracefully skipped."
|
|
6342
7016
|
};
|
|
6343
7017
|
}
|
|
6344
|
-
function checkVulnerabilities(rootDir, options) {
|
|
7018
|
+
function checkVulnerabilities(rootDir, options, vexStatements) {
|
|
6345
7019
|
if (options.scanVulnerabilities === false) {
|
|
6346
7020
|
return { vulnerabilities: [], blocked: false };
|
|
6347
7021
|
}
|
|
@@ -6349,7 +7023,10 @@ function checkVulnerabilities(rootDir, options) {
|
|
|
6349
7023
|
if (!res.executed) {
|
|
6350
7024
|
return buildSkippedVulnResult();
|
|
6351
7025
|
}
|
|
6352
|
-
const evalResult = evaluateVulnerabilityThresholds(res.vulnerabilities,
|
|
7026
|
+
const evalResult = evaluateVulnerabilityThresholds(res.vulnerabilities, {
|
|
7027
|
+
...options.vulnerabilityThresholds,
|
|
7028
|
+
vexStatements
|
|
7029
|
+
});
|
|
6353
7030
|
return { vulnerabilities: res.vulnerabilities, blocked: evalResult.blocked };
|
|
6354
7031
|
}
|
|
6355
7032
|
function assembleDynamicLicenseResult(dependencies, permittedCount, violations, reportMarkdown, artifacts, vulnData) {
|
|
@@ -6373,7 +7050,8 @@ function toCleanPolicy(policyWithRaw) {
|
|
|
6373
7050
|
permitted: policyWithRaw.permitted,
|
|
6374
7051
|
restricted: policyWithRaw.restricted,
|
|
6375
7052
|
blocked: policyWithRaw.blocked,
|
|
6376
|
-
exception_packages: policyWithRaw.exception_packages
|
|
7053
|
+
exception_packages: policyWithRaw.exception_packages,
|
|
7054
|
+
vex_statements: policyWithRaw.vex_statements
|
|
6377
7055
|
};
|
|
6378
7056
|
}
|
|
6379
7057
|
function scanDependenciesForVerification(options, rootDir, rawPolicy) {
|
|
@@ -6393,8 +7071,15 @@ function verifyLicenses(options = {}) {
|
|
|
6393
7071
|
}
|
|
6394
7072
|
const dependencies = scanDependenciesForVerification(options, rootDir, policyWithRaw.rawPolicy);
|
|
6395
7073
|
const { violations, permittedCount } = evaluateDependencyList(dependencies, policy);
|
|
6396
|
-
const
|
|
6397
|
-
const
|
|
7074
|
+
const vulnData = checkVulnerabilities(rootDir, options, policy.vex_statements);
|
|
7075
|
+
const artifacts = handleDynamicScaArtifacts(
|
|
7076
|
+
options,
|
|
7077
|
+
rootDir,
|
|
7078
|
+
policyWithRaw.rawPolicy,
|
|
7079
|
+
dependencies,
|
|
7080
|
+
vulnData.vulnerabilities,
|
|
7081
|
+
policy.vex_statements
|
|
7082
|
+
);
|
|
6398
7083
|
const reportMarkdown = generateLicenseReportMarkdown(dependencies, violations, policy, artifacts.sbomPath);
|
|
6399
7084
|
return assembleDynamicLicenseResult(dependencies, permittedCount, violations, reportMarkdown, artifacts, vulnData);
|
|
6400
7085
|
}
|
|
@@ -15934,9 +16619,149 @@ function initProject(options = {}) {
|
|
|
15934
16619
|
tryInstallGitHook(destDir, options.dryRun)
|
|
15935
16620
|
);
|
|
15936
16621
|
}
|
|
16622
|
+
var DEFAULT_DLP_RULES = [
|
|
16623
|
+
{
|
|
16624
|
+
id: "DLP-001-PRIVATE-KEY",
|
|
16625
|
+
description: "PEM / RSA / EC / OPENSSH Private Key Block",
|
|
16626
|
+
pattern: "-----BEGIN (?:[A-Z0-9_-]+ )?PRIVATE KEY-----[\\s\\S]*?-----END (?:[A-Z0-9_-]+ )?PRIVATE KEY-----",
|
|
16627
|
+
replacement: "[REDACTED:PRIVATE_KEY]",
|
|
16628
|
+
enabled: true
|
|
16629
|
+
},
|
|
16630
|
+
{
|
|
16631
|
+
id: "DLP-002-API-TOKEN",
|
|
16632
|
+
description: "Cloud, GitHub, Slack, OpenAI, Stripe, and AWS Access Tokens",
|
|
16633
|
+
pattern: "\\b(?:gh[pousr]_[A-Za-z0-9_]{36,255}|github_pat_[0-9a-zA-Z_]{82}|sk-(?:proj-|live-)?[a-zA-Z0-9_-]{32,}|AKIA[0-9A-Z]{16}|xox[baprs]-[0-9]{10,13}-[0-9]{10,13}[a-zA-Z0-9-]*|(?:sk|rk)_(?:live|test)_[0-9a-zA-Z]{24,99})\\b",
|
|
16634
|
+
replacement: "[REDACTED:API_TOKEN]",
|
|
16635
|
+
enabled: true
|
|
16636
|
+
},
|
|
16637
|
+
{
|
|
16638
|
+
id: "DLP-003-JWT",
|
|
16639
|
+
description: "Plaintext JSON Web Token (JWT)",
|
|
16640
|
+
pattern: "\\beyJ[A-Za-z0-9_-]{10,}\\.eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\b",
|
|
16641
|
+
replacement: "[REDACTED:JWT_TOKEN]",
|
|
16642
|
+
enabled: true
|
|
16643
|
+
},
|
|
16644
|
+
{
|
|
16645
|
+
id: "DLP-004-CREDIT-CARD",
|
|
16646
|
+
description: "Payment Card Number (13-19 digits formatted)",
|
|
16647
|
+
pattern: "\\b(?:\\d{4}[- ]\\d{4}[- ]\\d{4}[- ]\\d{4}|\\d{16})\\b",
|
|
16648
|
+
replacement: "[REDACTED:CREDIT_CARD]",
|
|
16649
|
+
enabled: true
|
|
16650
|
+
},
|
|
16651
|
+
{
|
|
16652
|
+
id: "DLP-005-EMAIL",
|
|
16653
|
+
description: "Personal or Corporate Email Address",
|
|
16654
|
+
pattern: "\\b[A-Za-z0-9._%+-]+@(?!example\\.com|localhost|test\\.local)[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\\b",
|
|
16655
|
+
replacement: "[REDACTED:EMAIL]",
|
|
16656
|
+
enabled: true
|
|
16657
|
+
},
|
|
16658
|
+
{
|
|
16659
|
+
id: "DLP-006-INTERNAL-URL",
|
|
16660
|
+
description: "Internal Intranet / Corp Service URL",
|
|
16661
|
+
pattern: "https?://(?:[A-Za-z0-9._-]+:[A-Za-z0-9._-]+@|[A-Za-z0-9.-]+\\.(?:internal|corp|intranet|local)\\b)[^\\s\"'`<>]*",
|
|
16662
|
+
replacement: "[REDACTED:INTERNAL_URL]",
|
|
16663
|
+
enabled: true
|
|
16664
|
+
}
|
|
16665
|
+
];
|
|
16666
|
+
function createDefaultDlpPolicy() {
|
|
16667
|
+
return {
|
|
16668
|
+
version: "1.0",
|
|
16669
|
+
enabled: true,
|
|
16670
|
+
zero_data_retention_required: true,
|
|
16671
|
+
rules: [...DEFAULT_DLP_RULES]
|
|
16672
|
+
};
|
|
16673
|
+
}
|
|
16674
|
+
function normalizeParsedRule(raw, idx) {
|
|
16675
|
+
if (typeof raw.pattern !== "string" || !raw.pattern) return null;
|
|
16676
|
+
return {
|
|
16677
|
+
id: typeof raw.id === "string" ? raw.id : `DLP-CUSTOM-${idx + 1}`,
|
|
16678
|
+
description: typeof raw.description === "string" ? raw.description : "Custom DLP rule",
|
|
16679
|
+
pattern: raw.pattern,
|
|
16680
|
+
replacement: typeof raw.replacement === "string" ? raw.replacement : "[REDACTED]",
|
|
16681
|
+
enabled: raw.enabled !== false
|
|
16682
|
+
};
|
|
16683
|
+
}
|
|
16684
|
+
function parseDlpPolicy(yamlContent) {
|
|
16685
|
+
const fallback = createDefaultDlpPolicy();
|
|
16686
|
+
if (!yamlContent || !yamlContent.trim()) return fallback;
|
|
16687
|
+
try {
|
|
16688
|
+
const doc = yaml3__default.default.load(yamlContent);
|
|
16689
|
+
if (!doc || typeof doc !== "object") return fallback;
|
|
16690
|
+
const rawRules = Array.isArray(doc.rules) ? doc.rules : [];
|
|
16691
|
+
const parsedRules = rawRules.map((r, i) => normalizeParsedRule(r, i)).filter((r) => r !== null);
|
|
16692
|
+
return {
|
|
16693
|
+
version: typeof doc.version === "string" ? doc.version : "1.0",
|
|
16694
|
+
enabled: doc.enabled !== false,
|
|
16695
|
+
zero_data_retention_required: Boolean(doc.zero_data_retention_required ?? true),
|
|
16696
|
+
rules: parsedRules.length > 0 ? parsedRules : fallback.rules
|
|
16697
|
+
};
|
|
16698
|
+
} catch {
|
|
16699
|
+
return fallback;
|
|
16700
|
+
}
|
|
16701
|
+
}
|
|
16702
|
+
function loadDlpPolicy(rootDir, customPolicyPath) {
|
|
16703
|
+
const base = rootDir || process.cwd();
|
|
16704
|
+
const filePath = customPolicyPath ? path4__namespace.isAbsolute(customPolicyPath) ? customPolicyPath : path4__namespace.join(base, customPolicyPath) : path4__namespace.join(base, "dlp-policy.yaml");
|
|
16705
|
+
if (!fs7__namespace.existsSync(filePath)) return createDefaultDlpPolicy();
|
|
16706
|
+
try {
|
|
16707
|
+
return parseDlpPolicy(fs7__namespace.readFileSync(filePath, "utf-8"));
|
|
16708
|
+
} catch {
|
|
16709
|
+
return createDefaultDlpPolicy();
|
|
16710
|
+
}
|
|
16711
|
+
}
|
|
16712
|
+
function applySingleDlpRule(text, rule) {
|
|
16713
|
+
if (rule.enabled === false) return { output: text, matches: 0 };
|
|
16714
|
+
try {
|
|
16715
|
+
const regex = new RegExp(rule.pattern, "g");
|
|
16716
|
+
let matches = 0;
|
|
16717
|
+
const output = text.replace(regex, () => {
|
|
16718
|
+
matches++;
|
|
16719
|
+
return rule.replacement;
|
|
16720
|
+
});
|
|
16721
|
+
return { output, matches };
|
|
16722
|
+
} catch {
|
|
16723
|
+
return { output: text, matches: 0 };
|
|
16724
|
+
}
|
|
16725
|
+
}
|
|
16726
|
+
function sanitizeDlpText(text, policy = createDefaultDlpPolicy()) {
|
|
16727
|
+
if (!policy.enabled || !text) {
|
|
16728
|
+
return { sanitizedText: text, redactionCount: 0, triggeredRuleIds: [] };
|
|
16729
|
+
}
|
|
16730
|
+
let current = text;
|
|
16731
|
+
let totalRedactions = 0;
|
|
16732
|
+
const triggered = [];
|
|
16733
|
+
for (const rule of policy.rules) {
|
|
16734
|
+
const res = applySingleDlpRule(current, rule);
|
|
16735
|
+
if (res.matches > 0) {
|
|
16736
|
+
current = res.output;
|
|
16737
|
+
totalRedactions += res.matches;
|
|
16738
|
+
triggered.push(rule.id);
|
|
16739
|
+
}
|
|
16740
|
+
}
|
|
16741
|
+
return {
|
|
16742
|
+
sanitizedText: current,
|
|
16743
|
+
redactionCount: totalRedactions,
|
|
16744
|
+
triggeredRuleIds: triggered
|
|
16745
|
+
};
|
|
16746
|
+
}
|
|
16747
|
+
function sanitizeDlpPayload(payload, policy = createDefaultDlpPolicy()) {
|
|
16748
|
+
if (!policy.enabled || payload === null || payload === void 0) return payload;
|
|
16749
|
+
if (typeof payload === "string") {
|
|
16750
|
+
return sanitizeDlpText(payload, policy).sanitizedText;
|
|
16751
|
+
}
|
|
16752
|
+
const serialized = JSON.stringify(payload);
|
|
16753
|
+
const { sanitizedText } = sanitizeDlpText(serialized, policy);
|
|
16754
|
+
try {
|
|
16755
|
+
return JSON.parse(sanitizedText);
|
|
16756
|
+
} catch {
|
|
16757
|
+
return payload;
|
|
16758
|
+
}
|
|
16759
|
+
}
|
|
15937
16760
|
|
|
15938
16761
|
exports.ALL_ARCHITECTURE_TEMPLATES = ALL_ARCHITECTURE_TEMPLATES;
|
|
15939
16762
|
exports.BRANCH_PATTERNS = BRANCH_PATTERNS;
|
|
16763
|
+
exports.BddStepRegistry = BddStepRegistry;
|
|
16764
|
+
exports.DEFAULT_DLP_RULES = DEFAULT_DLP_RULES;
|
|
15940
16765
|
exports.DEFAULT_EXCLUDED_PATTERNS = DEFAULT_EXCLUDED_PATTERNS;
|
|
15941
16766
|
exports.DEFAULT_EXCLUDES = DEFAULT_EXCLUDES;
|
|
15942
16767
|
exports.DEFAULT_EXTENSIONS = DEFAULT_EXTENSIONS;
|
|
@@ -16082,6 +16907,7 @@ exports.analyzeBddVariantsForRequirement = analyzeBddVariantsForRequirement;
|
|
|
16082
16907
|
exports.appendRevisionHistoryRow = appendRevisionHistoryRow;
|
|
16083
16908
|
exports.applyRatchetMode = applyRatchetMode;
|
|
16084
16909
|
exports.buildCycloneDxComponent = buildCycloneDxComponent;
|
|
16910
|
+
exports.buildCycloneDxVulnerabilities = buildCycloneDxVulnerabilities;
|
|
16085
16911
|
exports.buildFeatureFileContent = buildFeatureFileContent;
|
|
16086
16912
|
exports.buildGraphElements = buildGraphElements;
|
|
16087
16913
|
exports.calculateJaccardSimilarity = calculateJaccardSimilarity;
|
|
@@ -16098,6 +16924,7 @@ exports.collectHistoricalKpis = collectHistoricalKpis;
|
|
|
16098
16924
|
exports.computeCanonicalSha256 = computeCanonicalSha256;
|
|
16099
16925
|
exports.computeSha256 = computeSha256;
|
|
16100
16926
|
exports.computeViolationSignature = computeViolationSignature;
|
|
16927
|
+
exports.createDefaultDlpPolicy = createDefaultDlpPolicy;
|
|
16101
16928
|
exports.createDefaultQualityPolicy = createDefaultQualityPolicy;
|
|
16102
16929
|
exports.depositProductHandoffSidecar = depositProductHandoffSidecar;
|
|
16103
16930
|
exports.detectActiveChangeForIntegration = detectActiveChangeForIntegration;
|
|
@@ -16109,6 +16936,7 @@ exports.emitCiOutput = emitCiOutput;
|
|
|
16109
16936
|
exports.evaluateCompoundLicense = evaluateCompoundLicense;
|
|
16110
16937
|
exports.evaluateSingleDependency = evaluateSingleDependency;
|
|
16111
16938
|
exports.evaluateVulnerabilityThresholds = evaluateVulnerabilityThresholds;
|
|
16939
|
+
exports.executeBddFeature = executeBddFeature;
|
|
16112
16940
|
exports.extractBaselineEntriesFromResults = extractBaselineEntriesFromResults;
|
|
16113
16941
|
exports.extractCitations = extractCitations;
|
|
16114
16942
|
exports.extractCommitKpisFromRange = extractCommitKpisFromRange;
|
|
@@ -16163,6 +16991,7 @@ exports.isPlaceholderOrIgnored = isPlaceholderOrIgnored;
|
|
|
16163
16991
|
exports.isRequirementArtifact = isRequirementArtifact2;
|
|
16164
16992
|
exports.isSemgrepInstalled = isSemgrepInstalled;
|
|
16165
16993
|
exports.isToolAvailable = isToolAvailable;
|
|
16994
|
+
exports.loadDlpPolicy = loadDlpPolicy;
|
|
16166
16995
|
exports.loadPrivacyPolicy = loadPrivacyPolicy;
|
|
16167
16996
|
exports.loadProductHandoffSidecar = loadProductHandoffSidecar;
|
|
16168
16997
|
exports.loadQualityBaseline = loadQualityBaseline;
|
|
@@ -16172,6 +17001,8 @@ exports.maskSecret = maskSecret;
|
|
|
16172
17001
|
exports.normalizeSpdxLicense = normalizeSpdxLicense;
|
|
16173
17002
|
exports.normalizeUri = normalizeUri;
|
|
16174
17003
|
exports.parseCommitTrailers = parseCommitTrailers;
|
|
17004
|
+
exports.parseDlpPolicy = parseDlpPolicy;
|
|
17005
|
+
exports.parseFeatureDocument = parseFeatureDocument;
|
|
16175
17006
|
exports.parseFrontmatter = parseFrontmatter;
|
|
16176
17007
|
exports.parseFrontmatterId = parseFrontmatterId;
|
|
16177
17008
|
exports.parseGenericDoc = parseGenericDoc;
|
|
@@ -16213,15 +17044,21 @@ exports.resolveSchemaForArtifact = resolveSchemaForArtifact;
|
|
|
16213
17044
|
exports.resolveTargetFiles = resolveTargetFiles;
|
|
16214
17045
|
exports.runGitleaksCli = runGitleaksCli;
|
|
16215
17046
|
exports.runSemgrepCli = runSemgrepCli;
|
|
17047
|
+
exports.sanitizeDlpPayload = sanitizeDlpPayload;
|
|
17048
|
+
exports.sanitizeDlpText = sanitizeDlpText;
|
|
16216
17049
|
exports.scaffoldSddChange = scaffoldSddChange;
|
|
16217
17050
|
exports.scanAllProductHandoffs = scanAllProductHandoffs;
|
|
16218
17051
|
exports.scanAllTestArtifacts = scanAllTestArtifacts;
|
|
16219
17052
|
exports.scanContentForSecrets = scanContentForSecrets;
|
|
16220
17053
|
exports.scanFileForPrivacyViolations = scanFileForPrivacyViolations;
|
|
16221
17054
|
exports.scanFileForSast = scanFileForSast;
|
|
17055
|
+
exports.scanGoDependencies = scanGoDependencies;
|
|
16222
17056
|
exports.scanInstalledLicenses = scanInstalledLicenses;
|
|
16223
17057
|
exports.scanManualsInWorkspace = scanManualsInWorkspace;
|
|
17058
|
+
exports.scanMavenDependencies = scanMavenDependencies;
|
|
16224
17059
|
exports.scanPnpmStore = scanPnpmStore;
|
|
17060
|
+
exports.scanPythonDependencies = scanPythonDependencies;
|
|
17061
|
+
exports.scanRustDependencies = scanRustDependencies;
|
|
16225
17062
|
exports.scanSubdirectories = scanSubdirectories;
|
|
16226
17063
|
exports.scanWithNativeFs = scanWithNativeFs;
|
|
16227
17064
|
exports.scanWithSyft = scanWithSyft;
|
|
@@ -16252,6 +17089,7 @@ exports.walkMdFiles = walkMdFiles;
|
|
|
16252
17089
|
exports.walkProjectFiles = walkProjectFiles;
|
|
16253
17090
|
exports.walkSourceFiles = walkSourceFiles;
|
|
16254
17091
|
exports.walkTaskFiles = walkTaskFiles;
|
|
17092
|
+
exports.writeCucumberJsonReport = writeCucumberJsonReport;
|
|
16255
17093
|
exports.writeCycloneDxSbom = writeCycloneDxSbom;
|
|
16256
17094
|
exports.writeQualityBaseline = writeQualityBaseline;
|
|
16257
17095
|
exports.writeReleaseKpiReport = writeReleaseKpiReport;
|