@gitgov/core 1.10.1 → 1.11.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/src/index.d.ts +1087 -415
- package/dist/src/index.js +882 -23
- package/dist/src/index.js.map +1 -1
- package/package.json +2 -1
package/dist/src/index.js
CHANGED
|
@@ -3,16 +3,18 @@ import addFormats from 'ajv-formats';
|
|
|
3
3
|
import * as fs7 from 'fs';
|
|
4
4
|
import { promises, existsSync, constants, readFileSync, writeFileSync } from 'fs';
|
|
5
5
|
import * as yaml from 'js-yaml';
|
|
6
|
-
import { generateKeyPair, createHash, sign, verify } from 'crypto';
|
|
6
|
+
import { generateKeyPair, randomUUID, createHash, sign, verify } from 'crypto';
|
|
7
7
|
import { promisify } from 'util';
|
|
8
8
|
import * as path6 from 'path';
|
|
9
9
|
import path6__default, { join, basename, dirname } from 'path';
|
|
10
10
|
import { createRequire } from 'module';
|
|
11
11
|
import { fileURLToPath } from 'url';
|
|
12
|
+
import * as fs11 from 'fs/promises';
|
|
12
13
|
import { readdir } from 'fs/promises';
|
|
13
|
-
import { exec } from 'child_process';
|
|
14
|
+
import { exec, execSync } from 'child_process';
|
|
14
15
|
import os from 'os';
|
|
15
16
|
import { EventEmitter } from 'events';
|
|
17
|
+
import fg from 'fast-glob';
|
|
16
18
|
|
|
17
19
|
var __defProp = Object.defineProperty;
|
|
18
20
|
var __export = (target, all) => {
|
|
@@ -3305,6 +3307,37 @@ var ConfigManager = class _ConfigManager {
|
|
|
3305
3307
|
}
|
|
3306
3308
|
await promises.writeFile(this.sessionPath, JSON.stringify(session, null, 2), "utf-8");
|
|
3307
3309
|
}
|
|
3310
|
+
/**
|
|
3311
|
+
* Get audit state from config.json
|
|
3312
|
+
* Returns last full audit commit and timestamp for incremental mode
|
|
3313
|
+
*/
|
|
3314
|
+
async getAuditState() {
|
|
3315
|
+
const config = await this.loadConfig();
|
|
3316
|
+
return {
|
|
3317
|
+
lastFullAuditCommit: config?.state?.audit?.lastFullAuditCommit || null,
|
|
3318
|
+
lastFullAuditTimestamp: config?.state?.audit?.lastFullAuditTimestamp || null,
|
|
3319
|
+
lastFullAuditFindingsCount: config?.state?.audit?.lastFullAuditFindingsCount ?? null
|
|
3320
|
+
};
|
|
3321
|
+
}
|
|
3322
|
+
/**
|
|
3323
|
+
* Update audit state in config.json after a full audit
|
|
3324
|
+
* This is used to enable incremental audits
|
|
3325
|
+
*/
|
|
3326
|
+
async updateAuditState(auditState) {
|
|
3327
|
+
const config = await this.loadConfig();
|
|
3328
|
+
if (!config) {
|
|
3329
|
+
throw new Error("Cannot update audit state: config.json not found");
|
|
3330
|
+
}
|
|
3331
|
+
if (!config.state) {
|
|
3332
|
+
config.state = {};
|
|
3333
|
+
}
|
|
3334
|
+
config.state.audit = {
|
|
3335
|
+
lastFullAuditCommit: auditState.lastFullAuditCommit,
|
|
3336
|
+
lastFullAuditTimestamp: auditState.lastFullAuditTimestamp,
|
|
3337
|
+
lastFullAuditFindingsCount: auditState.lastFullAuditFindingsCount
|
|
3338
|
+
};
|
|
3339
|
+
await promises.writeFile(this.configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
3340
|
+
}
|
|
3308
3341
|
/**
|
|
3309
3342
|
* Finds the project root by searching upwards for a .git directory.
|
|
3310
3343
|
* Caches the result for subsequent calls.
|
|
@@ -7510,8 +7543,8 @@ var ProjectAdapter = class {
|
|
|
7510
7543
|
/**
|
|
7511
7544
|
* [EARS-2] Validates environment for GitGovernance initialization
|
|
7512
7545
|
*/
|
|
7513
|
-
async validateEnvironment(
|
|
7514
|
-
const targetPath =
|
|
7546
|
+
async validateEnvironment(path10) {
|
|
7547
|
+
const targetPath = path10 || process.env["GITGOV_ORIGINAL_DIR"] || process.cwd();
|
|
7515
7548
|
const warnings = [];
|
|
7516
7549
|
const suggestions = [];
|
|
7517
7550
|
try {
|
|
@@ -9924,22 +9957,22 @@ var LintModule = class {
|
|
|
9924
9957
|
);
|
|
9925
9958
|
}
|
|
9926
9959
|
this.fileSystem = dependencies.fileSystem ?? {
|
|
9927
|
-
readFile: async (
|
|
9928
|
-
return promises.readFile(
|
|
9960
|
+
readFile: async (path10, encoding) => {
|
|
9961
|
+
return promises.readFile(path10, encoding);
|
|
9929
9962
|
},
|
|
9930
|
-
writeFile: async (
|
|
9931
|
-
await promises.writeFile(
|
|
9963
|
+
writeFile: async (path10, content) => {
|
|
9964
|
+
await promises.writeFile(path10, content, "utf-8");
|
|
9932
9965
|
},
|
|
9933
|
-
exists: async (
|
|
9966
|
+
exists: async (path10) => {
|
|
9934
9967
|
try {
|
|
9935
|
-
await promises.access(
|
|
9968
|
+
await promises.access(path10);
|
|
9936
9969
|
return true;
|
|
9937
9970
|
} catch {
|
|
9938
9971
|
return false;
|
|
9939
9972
|
}
|
|
9940
9973
|
},
|
|
9941
|
-
unlink: async (
|
|
9942
|
-
await promises.unlink(
|
|
9974
|
+
unlink: async (path10) => {
|
|
9975
|
+
await promises.unlink(path10);
|
|
9943
9976
|
}
|
|
9944
9977
|
};
|
|
9945
9978
|
}
|
|
@@ -10447,8 +10480,8 @@ var LintModule = class {
|
|
|
10447
10480
|
* This ensures we know the correct type for each record based on its directory.
|
|
10448
10481
|
* @private
|
|
10449
10482
|
*/
|
|
10450
|
-
async discoverAllRecordsWithTypes(
|
|
10451
|
-
const projectRoot2 = ConfigManager.findProjectRoot() ||
|
|
10483
|
+
async discoverAllRecordsWithTypes(path10) {
|
|
10484
|
+
const projectRoot2 = ConfigManager.findProjectRoot() || path10 || ".gitgov/";
|
|
10452
10485
|
const recordTypes = [
|
|
10453
10486
|
"actor",
|
|
10454
10487
|
"agent",
|
|
@@ -14020,10 +14053,10 @@ var RelationshipAnalyzer = class {
|
|
|
14020
14053
|
}
|
|
14021
14054
|
const visited = /* @__PURE__ */ new Set();
|
|
14022
14055
|
const recursionStack = /* @__PURE__ */ new Set();
|
|
14023
|
-
const
|
|
14056
|
+
const path10 = [];
|
|
14024
14057
|
for (const node of graph.keys()) {
|
|
14025
14058
|
if (!visited.has(node)) {
|
|
14026
|
-
const cyclePath = this.findCycleDFS(node, graph, visited, recursionStack,
|
|
14059
|
+
const cyclePath = this.findCycleDFS(node, graph, visited, recursionStack, path10);
|
|
14027
14060
|
if (cyclePath.length > 0) {
|
|
14028
14061
|
const cycleDescription = this.formatCycleError(cyclePath);
|
|
14029
14062
|
throw new CircularDependencyError(cycleDescription);
|
|
@@ -14034,24 +14067,24 @@ var RelationshipAnalyzer = class {
|
|
|
14034
14067
|
/**
|
|
14035
14068
|
* DFS helper for circular dependency detection with path tracking
|
|
14036
14069
|
*/
|
|
14037
|
-
findCycleDFS(node, graph, visited, recursionStack,
|
|
14070
|
+
findCycleDFS(node, graph, visited, recursionStack, path10) {
|
|
14038
14071
|
visited.add(node);
|
|
14039
14072
|
recursionStack.add(node);
|
|
14040
|
-
|
|
14073
|
+
path10.push(node);
|
|
14041
14074
|
const neighbors = graph.get(node) || [];
|
|
14042
14075
|
for (const neighbor of neighbors) {
|
|
14043
14076
|
if (!visited.has(neighbor)) {
|
|
14044
|
-
const cyclePath = this.findCycleDFS(neighbor, graph, visited, recursionStack,
|
|
14077
|
+
const cyclePath = this.findCycleDFS(neighbor, graph, visited, recursionStack, path10);
|
|
14045
14078
|
if (cyclePath.length > 0) {
|
|
14046
14079
|
return cyclePath;
|
|
14047
14080
|
}
|
|
14048
14081
|
} else if (recursionStack.has(neighbor)) {
|
|
14049
|
-
const cycleStartIndex =
|
|
14050
|
-
return
|
|
14082
|
+
const cycleStartIndex = path10.indexOf(neighbor);
|
|
14083
|
+
return path10.slice(cycleStartIndex).concat([neighbor]);
|
|
14051
14084
|
}
|
|
14052
14085
|
}
|
|
14053
14086
|
recursionStack.delete(node);
|
|
14054
|
-
|
|
14087
|
+
path10.pop();
|
|
14055
14088
|
return [];
|
|
14056
14089
|
}
|
|
14057
14090
|
/**
|
|
@@ -14759,6 +14792,832 @@ var DiagramGenerator = class {
|
|
|
14759
14792
|
}
|
|
14760
14793
|
};
|
|
14761
14794
|
|
|
14762
|
-
|
|
14795
|
+
// src/pii_detector/index.ts
|
|
14796
|
+
var pii_detector_exports = {};
|
|
14797
|
+
__export(pii_detector_exports, {
|
|
14798
|
+
HeuristicDetector: () => HeuristicDetector,
|
|
14799
|
+
HttpLlmDetector: () => HttpLlmDetector,
|
|
14800
|
+
PiiDetectorModule: () => PiiDetectorModule,
|
|
14801
|
+
REGEX_RULES: () => REGEX_RULES,
|
|
14802
|
+
RegexDetector: () => RegexDetector
|
|
14803
|
+
});
|
|
14804
|
+
|
|
14805
|
+
// src/pii_detector/rules/regex_rules.ts
|
|
14806
|
+
var REGEX_RULES = [
|
|
14807
|
+
// === PII ===
|
|
14808
|
+
{
|
|
14809
|
+
id: "PII-001",
|
|
14810
|
+
pattern: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
|
|
14811
|
+
category: "pii-email",
|
|
14812
|
+
severity: "high",
|
|
14813
|
+
message: "Email address detected in source code",
|
|
14814
|
+
suggestion: "Move to configuration or environment variable",
|
|
14815
|
+
legalReference: "GDPR Art. 4(1)"
|
|
14816
|
+
},
|
|
14817
|
+
{
|
|
14818
|
+
id: "PII-002",
|
|
14819
|
+
pattern: /(\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/g,
|
|
14820
|
+
category: "pii-phone",
|
|
14821
|
+
severity: "medium",
|
|
14822
|
+
message: "Phone number pattern detected",
|
|
14823
|
+
suggestion: "Avoid hardcoding personal phone numbers"
|
|
14824
|
+
},
|
|
14825
|
+
{
|
|
14826
|
+
id: "PII-003",
|
|
14827
|
+
pattern: /\b(?:\d{4}[-\s]?){3}\d{4}\b/g,
|
|
14828
|
+
category: "pii-financial",
|
|
14829
|
+
severity: "critical",
|
|
14830
|
+
message: "Potential credit card number detected",
|
|
14831
|
+
suggestion: "Never store credit card numbers in source code",
|
|
14832
|
+
legalReference: "PCI-DSS, GDPR Art. 32"
|
|
14833
|
+
},
|
|
14834
|
+
{
|
|
14835
|
+
id: "PII-004",
|
|
14836
|
+
pattern: /\b\d{3}-\d{2}-\d{4}\b/g,
|
|
14837
|
+
category: "pii-generic",
|
|
14838
|
+
severity: "critical",
|
|
14839
|
+
message: "US Social Security Number pattern detected",
|
|
14840
|
+
suggestion: "SSNs must never be stored in source code"
|
|
14841
|
+
},
|
|
14842
|
+
{
|
|
14843
|
+
id: "PII-005",
|
|
14844
|
+
pattern: /\b(ssn|dni|document_number|iban)\b/gi,
|
|
14845
|
+
category: "pii-generic",
|
|
14846
|
+
severity: "medium",
|
|
14847
|
+
message: "Sensitive field name detected",
|
|
14848
|
+
suggestion: "Review if real data or structure requiring encryption"
|
|
14849
|
+
},
|
|
14850
|
+
// === SECRETS ===
|
|
14851
|
+
{
|
|
14852
|
+
id: "SEC-001",
|
|
14853
|
+
pattern: /(?:api[_-]?key|apikey|secret[_-]?key)\s*[:=]\s*['"][^'"]{20,}['"]/gi,
|
|
14854
|
+
category: "hardcoded-secret",
|
|
14855
|
+
severity: "critical",
|
|
14856
|
+
message: "Hardcoded API key detected",
|
|
14857
|
+
suggestion: "Use environment variables or secret management"
|
|
14858
|
+
},
|
|
14859
|
+
{
|
|
14860
|
+
id: "SEC-002",
|
|
14861
|
+
pattern: /AKIA[0-9A-Z]{16}/g,
|
|
14862
|
+
category: "hardcoded-secret",
|
|
14863
|
+
severity: "critical",
|
|
14864
|
+
message: "AWS Access Key ID detected",
|
|
14865
|
+
suggestion: "Rotate this key immediately and use IAM roles"
|
|
14866
|
+
},
|
|
14867
|
+
{
|
|
14868
|
+
id: "SEC-003",
|
|
14869
|
+
pattern: /-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/g,
|
|
14870
|
+
category: "hardcoded-secret",
|
|
14871
|
+
severity: "critical",
|
|
14872
|
+
message: "Private key detected in source code",
|
|
14873
|
+
suggestion: "Never commit private keys. Use secret management."
|
|
14874
|
+
},
|
|
14875
|
+
// === LOGGING PII ===
|
|
14876
|
+
{
|
|
14877
|
+
id: "LOG-001",
|
|
14878
|
+
pattern: /console\.(log|info|warn|error)\s*\([^)]*(?:email|password|ssn|phone|credit)/gi,
|
|
14879
|
+
category: "logging-pii",
|
|
14880
|
+
severity: "high",
|
|
14881
|
+
message: "Potential PII being logged",
|
|
14882
|
+
suggestion: "Sanitize logs to remove personal data",
|
|
14883
|
+
legalReference: "GDPR Art. 5(1)(f)"
|
|
14884
|
+
}
|
|
14885
|
+
];
|
|
14886
|
+
|
|
14887
|
+
// src/pii_detector/detectors/regex_detector.ts
|
|
14888
|
+
var MAX_SNIPPET_LENGTH = 300;
|
|
14889
|
+
function generateFingerprint(ruleId, file, line) {
|
|
14890
|
+
return createHash("sha256").update(`${ruleId}:${file}:${line}`).digest("hex");
|
|
14891
|
+
}
|
|
14892
|
+
function truncateSnippet(snippet) {
|
|
14893
|
+
if (snippet.length <= MAX_SNIPPET_LENGTH) {
|
|
14894
|
+
return snippet;
|
|
14895
|
+
}
|
|
14896
|
+
return snippet.slice(0, MAX_SNIPPET_LENGTH - 3) + "...";
|
|
14897
|
+
}
|
|
14898
|
+
function getLineNumber(content, index) {
|
|
14899
|
+
return content.slice(0, index).split("\n").length;
|
|
14900
|
+
}
|
|
14901
|
+
function extractSnippet(content, matchIndex) {
|
|
14902
|
+
const lines = content.split("\n");
|
|
14903
|
+
const lineNumber = getLineNumber(content, matchIndex);
|
|
14904
|
+
const line = lines[lineNumber - 1] || "";
|
|
14905
|
+
return truncateSnippet(line.trim());
|
|
14906
|
+
}
|
|
14907
|
+
var RegexDetector = class {
|
|
14908
|
+
name = "regex";
|
|
14909
|
+
rules;
|
|
14910
|
+
constructor(ruleIds) {
|
|
14911
|
+
if (ruleIds && ruleIds.length > 0) {
|
|
14912
|
+
this.rules = REGEX_RULES.filter((r) => ruleIds.includes(r.id));
|
|
14913
|
+
} else {
|
|
14914
|
+
this.rules = REGEX_RULES;
|
|
14915
|
+
}
|
|
14916
|
+
}
|
|
14917
|
+
async detect(content, filePath) {
|
|
14918
|
+
const findings = [];
|
|
14919
|
+
for (const rule of this.rules) {
|
|
14920
|
+
const pattern = new RegExp(rule.pattern.source, rule.pattern.flags);
|
|
14921
|
+
let match;
|
|
14922
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
14923
|
+
const line = getLineNumber(content, match.index);
|
|
14924
|
+
const snippet = extractSnippet(content, match.index);
|
|
14925
|
+
const finding = {
|
|
14926
|
+
id: randomUUID(),
|
|
14927
|
+
ruleId: rule.id,
|
|
14928
|
+
category: rule.category,
|
|
14929
|
+
severity: rule.severity,
|
|
14930
|
+
file: filePath,
|
|
14931
|
+
line,
|
|
14932
|
+
snippet,
|
|
14933
|
+
message: rule.message,
|
|
14934
|
+
detector: this.name,
|
|
14935
|
+
fingerprint: generateFingerprint(rule.id, filePath, line),
|
|
14936
|
+
confidence: 1
|
|
14937
|
+
};
|
|
14938
|
+
if (rule.suggestion) finding.suggestion = rule.suggestion;
|
|
14939
|
+
if (rule.legalReference) finding.legalReference = rule.legalReference;
|
|
14940
|
+
findings.push(finding);
|
|
14941
|
+
}
|
|
14942
|
+
}
|
|
14943
|
+
return findings;
|
|
14944
|
+
}
|
|
14945
|
+
};
|
|
14946
|
+
var MAX_SNIPPET_LENGTH2 = 300;
|
|
14947
|
+
var SENSITIVE_VAR_PATTERN = /\b(user|customer|client|employee|patient)(_)?(email|phone|ssn|address|creditcard|password)\b/gi;
|
|
14948
|
+
var LOGGING_PATTERN = /console\.(log|info|debug|warn)\s*\([^)]*\b(user|customer|request\.body|formData)\b/gi;
|
|
14949
|
+
var SERIALIZE_PATTERN = /JSON\.stringify\s*\([^)]*\b(user|customer|profile|account)\b/gi;
|
|
14950
|
+
var HEURISTIC_RULES = [
|
|
14951
|
+
{
|
|
14952
|
+
id: "HEUR-001",
|
|
14953
|
+
pattern: SENSITIVE_VAR_PATTERN,
|
|
14954
|
+
category: "pii-generic",
|
|
14955
|
+
severity: "medium",
|
|
14956
|
+
confidence: 0.7,
|
|
14957
|
+
message: "Sensitive variable name detected",
|
|
14958
|
+
suggestion: "Consider if this variable contains actual PII"
|
|
14959
|
+
},
|
|
14960
|
+
{
|
|
14961
|
+
id: "HEUR-002",
|
|
14962
|
+
pattern: LOGGING_PATTERN,
|
|
14963
|
+
category: "logging-pii",
|
|
14964
|
+
severity: "medium",
|
|
14965
|
+
confidence: 0.6,
|
|
14966
|
+
message: "Logging of potentially sensitive object detected",
|
|
14967
|
+
suggestion: "Sanitize logged objects to remove PII"
|
|
14968
|
+
},
|
|
14969
|
+
{
|
|
14970
|
+
id: "HEUR-003",
|
|
14971
|
+
pattern: SERIALIZE_PATTERN,
|
|
14972
|
+
category: "third-party-transfer",
|
|
14973
|
+
severity: "low",
|
|
14974
|
+
confidence: 0.5,
|
|
14975
|
+
message: "JSON serialization of potentially sensitive object",
|
|
14976
|
+
suggestion: "Ensure sensitive fields are excluded before serialization"
|
|
14977
|
+
}
|
|
14978
|
+
];
|
|
14979
|
+
function generateFingerprint2(ruleId, file, line) {
|
|
14980
|
+
return createHash("sha256").update(`${ruleId}:${file}:${line}`).digest("hex");
|
|
14981
|
+
}
|
|
14982
|
+
function truncateSnippet2(snippet) {
|
|
14983
|
+
if (snippet.length <= MAX_SNIPPET_LENGTH2) {
|
|
14984
|
+
return snippet;
|
|
14985
|
+
}
|
|
14986
|
+
return snippet.slice(0, MAX_SNIPPET_LENGTH2 - 3) + "...";
|
|
14987
|
+
}
|
|
14988
|
+
function getLineNumber2(content, index) {
|
|
14989
|
+
return content.slice(0, index).split("\n").length;
|
|
14990
|
+
}
|
|
14991
|
+
function extractSnippet2(content, matchIndex) {
|
|
14992
|
+
const lines = content.split("\n");
|
|
14993
|
+
const lineNumber = getLineNumber2(content, matchIndex);
|
|
14994
|
+
const line = lines[lineNumber - 1] || "";
|
|
14995
|
+
return truncateSnippet2(line.trim());
|
|
14996
|
+
}
|
|
14997
|
+
var HeuristicDetector = class {
|
|
14998
|
+
name = "heuristic";
|
|
14999
|
+
async detect(content, filePath) {
|
|
15000
|
+
const findings = [];
|
|
15001
|
+
for (const rule of HEURISTIC_RULES) {
|
|
15002
|
+
const pattern = new RegExp(rule.pattern.source, rule.pattern.flags);
|
|
15003
|
+
let match;
|
|
15004
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
15005
|
+
const line = getLineNumber2(content, match.index);
|
|
15006
|
+
const snippet = extractSnippet2(content, match.index);
|
|
15007
|
+
const finding = {
|
|
15008
|
+
id: randomUUID(),
|
|
15009
|
+
ruleId: rule.id,
|
|
15010
|
+
category: rule.category,
|
|
15011
|
+
severity: rule.severity,
|
|
15012
|
+
file: filePath,
|
|
15013
|
+
line,
|
|
15014
|
+
snippet,
|
|
15015
|
+
message: rule.message,
|
|
15016
|
+
detector: this.name,
|
|
15017
|
+
fingerprint: generateFingerprint2(rule.id, filePath, line),
|
|
15018
|
+
confidence: rule.confidence
|
|
15019
|
+
};
|
|
15020
|
+
if (rule.suggestion) finding.suggestion = rule.suggestion;
|
|
15021
|
+
findings.push(finding);
|
|
15022
|
+
}
|
|
15023
|
+
}
|
|
15024
|
+
return findings;
|
|
15025
|
+
}
|
|
15026
|
+
};
|
|
15027
|
+
var MAX_SNIPPET_LENGTH3 = 300;
|
|
15028
|
+
function generateFingerprint3(ruleId, file, line) {
|
|
15029
|
+
return createHash("sha256").update(`${ruleId}:${file}:${line}`).digest("hex");
|
|
15030
|
+
}
|
|
15031
|
+
function truncateSnippet3(snippet) {
|
|
15032
|
+
if (snippet.length <= MAX_SNIPPET_LENGTH3) {
|
|
15033
|
+
return snippet;
|
|
15034
|
+
}
|
|
15035
|
+
return snippet.slice(0, MAX_SNIPPET_LENGTH3 - 3) + "...";
|
|
15036
|
+
}
|
|
15037
|
+
function isValidCategory(category) {
|
|
15038
|
+
const validCategories = [
|
|
15039
|
+
"pii-email",
|
|
15040
|
+
"pii-phone",
|
|
15041
|
+
"pii-financial",
|
|
15042
|
+
"pii-health",
|
|
15043
|
+
"pii-generic",
|
|
15044
|
+
"hardcoded-secret",
|
|
15045
|
+
"logging-pii",
|
|
15046
|
+
"tracking-cookie",
|
|
15047
|
+
"tracking-analytics-id",
|
|
15048
|
+
"unencrypted-storage",
|
|
15049
|
+
"third-party-transfer",
|
|
15050
|
+
"unknown-risk"
|
|
15051
|
+
];
|
|
15052
|
+
return validCategories.includes(category);
|
|
15053
|
+
}
|
|
15054
|
+
var HttpLlmDetector = class {
|
|
15055
|
+
endpoint;
|
|
15056
|
+
apiKey;
|
|
15057
|
+
constructor(endpoint, apiKey) {
|
|
15058
|
+
this.endpoint = endpoint;
|
|
15059
|
+
this.apiKey = apiKey;
|
|
15060
|
+
}
|
|
15061
|
+
/**
|
|
15062
|
+
* Analyzes code snippets with LLM for semantic PII detection.
|
|
15063
|
+
* Implements EARS-18: Send candidates to LLM when quota available
|
|
15064
|
+
* Implements EARS-19: Normalize LLM response to GdprFinding format
|
|
15065
|
+
*/
|
|
15066
|
+
async analyzeSnippets(snippets) {
|
|
15067
|
+
if (snippets.length === 0) {
|
|
15068
|
+
return [];
|
|
15069
|
+
}
|
|
15070
|
+
const response = await fetch(this.endpoint, {
|
|
15071
|
+
method: "POST",
|
|
15072
|
+
headers: {
|
|
15073
|
+
"Content-Type": "application/json",
|
|
15074
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
15075
|
+
},
|
|
15076
|
+
body: JSON.stringify({ snippets })
|
|
15077
|
+
});
|
|
15078
|
+
if (!response.ok) {
|
|
15079
|
+
throw new Error(`LLM API error: ${response.status} ${response.statusText}`);
|
|
15080
|
+
}
|
|
15081
|
+
const data = await response.json();
|
|
15082
|
+
return this.normalizeFindings(data.findings);
|
|
15083
|
+
}
|
|
15084
|
+
/**
|
|
15085
|
+
* Normalizes raw LLM findings to GdprFinding format.
|
|
15086
|
+
*/
|
|
15087
|
+
normalizeFindings(rawFindings) {
|
|
15088
|
+
return rawFindings.map((raw) => {
|
|
15089
|
+
const category = isValidCategory(raw.category) ? raw.category : "unknown-risk";
|
|
15090
|
+
const finding = {
|
|
15091
|
+
id: randomUUID(),
|
|
15092
|
+
ruleId: raw.ruleId ?? "LLM-001",
|
|
15093
|
+
category,
|
|
15094
|
+
severity: raw.severity,
|
|
15095
|
+
file: raw.file,
|
|
15096
|
+
line: raw.line,
|
|
15097
|
+
snippet: truncateSnippet3(raw.snippet ?? ""),
|
|
15098
|
+
message: raw.message,
|
|
15099
|
+
detector: "llm",
|
|
15100
|
+
fingerprint: generateFingerprint3(
|
|
15101
|
+
raw.ruleId ?? "LLM-001",
|
|
15102
|
+
raw.file,
|
|
15103
|
+
raw.line
|
|
15104
|
+
),
|
|
15105
|
+
confidence: raw.confidence ?? 0.9
|
|
15106
|
+
};
|
|
15107
|
+
if (raw.suggestion) finding.suggestion = raw.suggestion;
|
|
15108
|
+
if (raw.legalReference) finding.legalReference = raw.legalReference;
|
|
15109
|
+
return finding;
|
|
15110
|
+
});
|
|
15111
|
+
}
|
|
15112
|
+
};
|
|
15113
|
+
|
|
15114
|
+
// src/pii_detector/pii_detector.ts
|
|
15115
|
+
var PiiDetectorModule = class {
|
|
15116
|
+
localDetectors = [];
|
|
15117
|
+
llmDetector;
|
|
15118
|
+
llmConfig;
|
|
15119
|
+
/**
|
|
15120
|
+
* Constructs the module with graceful degradation.
|
|
15121
|
+
* Without config -> only RegexDetector (Free tier).
|
|
15122
|
+
*/
|
|
15123
|
+
constructor(config) {
|
|
15124
|
+
if (config?.regex?.enabled === false) ; else {
|
|
15125
|
+
this.localDetectors.push(new RegexDetector(config?.regex?.rules));
|
|
15126
|
+
}
|
|
15127
|
+
if (config?.heuristic?.enabled) {
|
|
15128
|
+
this.localDetectors.push(new HeuristicDetector());
|
|
15129
|
+
}
|
|
15130
|
+
if (config?.llm?.enabled && config.llm.endpoint) {
|
|
15131
|
+
this.llmConfig = config.llm;
|
|
15132
|
+
const apiKey = process.env["GITGOV_LLM_API_KEY"];
|
|
15133
|
+
if (apiKey) {
|
|
15134
|
+
this.llmDetector = new HttpLlmDetector(config.llm.endpoint, apiKey);
|
|
15135
|
+
}
|
|
15136
|
+
}
|
|
15137
|
+
}
|
|
15138
|
+
/**
|
|
15139
|
+
* Detects PII and secrets in file content.
|
|
15140
|
+
*
|
|
15141
|
+
* Flow:
|
|
15142
|
+
* 1. Run all enabled local detectors (Phase 1)
|
|
15143
|
+
* 2. Extract candidates with confidence < 0.8
|
|
15144
|
+
* 3. If LLM enabled and quota OK, analyze candidates (Phase 2)
|
|
15145
|
+
* 4. Merge and deduplicate by fingerprint
|
|
15146
|
+
*/
|
|
15147
|
+
async detect(content, filePath) {
|
|
15148
|
+
const localFindings = await this.runLocalDetectors(content, filePath);
|
|
15149
|
+
let llmFindings = [];
|
|
15150
|
+
if (this.llmDetector && this.checkQuota()) {
|
|
15151
|
+
const candidates = this.extractCandidates(localFindings, content, filePath);
|
|
15152
|
+
if (candidates.length > 0) {
|
|
15153
|
+
try {
|
|
15154
|
+
llmFindings = await this.llmDetector.analyzeSnippets(candidates);
|
|
15155
|
+
this.decrementQuota(candidates.length);
|
|
15156
|
+
} catch {
|
|
15157
|
+
}
|
|
15158
|
+
}
|
|
15159
|
+
}
|
|
15160
|
+
return this.deduplicateByFingerprint([...localFindings, ...llmFindings]);
|
|
15161
|
+
}
|
|
15162
|
+
/**
|
|
15163
|
+
* Runs all local detectors and collects findings.
|
|
15164
|
+
*/
|
|
15165
|
+
async runLocalDetectors(content, filePath) {
|
|
15166
|
+
const results = await Promise.all(
|
|
15167
|
+
this.localDetectors.map((d) => d.detect(content, filePath))
|
|
15168
|
+
);
|
|
15169
|
+
return results.flat();
|
|
15170
|
+
}
|
|
15171
|
+
/**
|
|
15172
|
+
* Extracts CodeSnippets from low-confidence findings for LLM analysis.
|
|
15173
|
+
* Includes 2 lines of context before and after.
|
|
15174
|
+
* Implements EARS-15: Extract candidates with confidence < 0.8
|
|
15175
|
+
*/
|
|
15176
|
+
extractCandidates(findings, content, filePath) {
|
|
15177
|
+
const lines = content.split("\n");
|
|
15178
|
+
const lang = this.detectLanguage(filePath);
|
|
15179
|
+
return findings.filter((f) => f.confidence < 0.8).map((f) => ({
|
|
15180
|
+
file: filePath,
|
|
15181
|
+
lineStart: Math.max(1, f.line - 2),
|
|
15182
|
+
lineEnd: Math.min(lines.length, f.line + 2),
|
|
15183
|
+
language: lang,
|
|
15184
|
+
content: lines.slice(Math.max(0, f.line - 3), f.line + 2).join("\n"),
|
|
15185
|
+
heuristicTags: [f.category, f.detector]
|
|
15186
|
+
}));
|
|
15187
|
+
}
|
|
15188
|
+
/**
|
|
15189
|
+
* Checks if LLM quota is available.
|
|
15190
|
+
* Implements EARS-20: Reject when trial expired
|
|
15191
|
+
* Implements EARS-21: Reject when remainingUses is zero
|
|
15192
|
+
*/
|
|
15193
|
+
checkQuota() {
|
|
15194
|
+
if (!this.llmConfig) return false;
|
|
15195
|
+
if (this.llmConfig.quotaType === "unlimited") return true;
|
|
15196
|
+
if (this.llmConfig.quotaType === "trial") {
|
|
15197
|
+
if (this.llmConfig.expiresAt) {
|
|
15198
|
+
const expired = new Date(this.llmConfig.expiresAt) < /* @__PURE__ */ new Date();
|
|
15199
|
+
if (expired) return false;
|
|
15200
|
+
}
|
|
15201
|
+
}
|
|
15202
|
+
return (this.llmConfig.remainingUses ?? 0) > 0;
|
|
15203
|
+
}
|
|
15204
|
+
/**
|
|
15205
|
+
* Decrements quota after successful LLM call.
|
|
15206
|
+
* Implements EARS-22: Decrement remainingUses after successful call
|
|
15207
|
+
*/
|
|
15208
|
+
decrementQuota(count) {
|
|
15209
|
+
if (this.llmConfig?.remainingUses !== void 0) {
|
|
15210
|
+
this.llmConfig.remainingUses = Math.max(
|
|
15211
|
+
0,
|
|
15212
|
+
this.llmConfig.remainingUses - count
|
|
15213
|
+
);
|
|
15214
|
+
}
|
|
15215
|
+
}
|
|
15216
|
+
/**
|
|
15217
|
+
* Deduplicates findings by SHA256 fingerprint.
|
|
15218
|
+
*/
|
|
15219
|
+
deduplicateByFingerprint(findings) {
|
|
15220
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15221
|
+
return findings.filter((f) => {
|
|
15222
|
+
if (seen.has(f.fingerprint)) return false;
|
|
15223
|
+
seen.add(f.fingerprint);
|
|
15224
|
+
return true;
|
|
15225
|
+
});
|
|
15226
|
+
}
|
|
15227
|
+
/**
|
|
15228
|
+
* Detects programming language based on file extension.
|
|
15229
|
+
*/
|
|
15230
|
+
detectLanguage(filePath) {
|
|
15231
|
+
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
15232
|
+
const map = {
|
|
15233
|
+
ts: "typescript",
|
|
15234
|
+
tsx: "typescript",
|
|
15235
|
+
js: "javascript",
|
|
15236
|
+
jsx: "javascript",
|
|
15237
|
+
py: "python",
|
|
15238
|
+
go: "go",
|
|
15239
|
+
java: "java",
|
|
15240
|
+
rs: "rust",
|
|
15241
|
+
rb: "ruby"
|
|
15242
|
+
};
|
|
15243
|
+
return map[ext ?? ""] ?? "unknown";
|
|
15244
|
+
}
|
|
15245
|
+
};
|
|
15246
|
+
|
|
15247
|
+
// src/source_auditor/index.ts
|
|
15248
|
+
var source_auditor_exports = {};
|
|
15249
|
+
__export(source_auditor_exports, {
|
|
15250
|
+
ScopeSelector: () => ScopeSelector,
|
|
15251
|
+
ScoringEngine: () => ScoringEngine,
|
|
15252
|
+
SourceAuditorModule: () => SourceAuditorModule,
|
|
15253
|
+
WaiverReader: () => WaiverReader,
|
|
15254
|
+
WaiverWriter: () => WaiverWriter
|
|
15255
|
+
});
|
|
15256
|
+
var ScopeSelector = class {
|
|
15257
|
+
/**
|
|
15258
|
+
* Selects files matching include patterns, excluding those matching exclude patterns.
|
|
15259
|
+
* Automatically respects .gitignore patterns from the project root.
|
|
15260
|
+
* If scope.changedSince is set, only returns files changed since that commit.
|
|
15261
|
+
* @param scope - Include and exclude glob patterns, optional changedSince commit
|
|
15262
|
+
* @param baseDir - Base directory for file search
|
|
15263
|
+
* @returns Array of file paths relative to baseDir
|
|
15264
|
+
*/
|
|
15265
|
+
async selectFiles(scope, baseDir) {
|
|
15266
|
+
if (scope.include.length === 0) {
|
|
15267
|
+
return [];
|
|
15268
|
+
}
|
|
15269
|
+
const gitignorePatterns = await this.loadGitignorePatterns(baseDir);
|
|
15270
|
+
const allExcludes = [...gitignorePatterns, ...scope.exclude];
|
|
15271
|
+
if (scope.changedSince) {
|
|
15272
|
+
return this.selectChangedFiles(scope.changedSince, allExcludes, baseDir);
|
|
15273
|
+
}
|
|
15274
|
+
const files = await fg(scope.include, {
|
|
15275
|
+
cwd: baseDir,
|
|
15276
|
+
ignore: allExcludes,
|
|
15277
|
+
onlyFiles: true,
|
|
15278
|
+
absolute: false
|
|
15279
|
+
});
|
|
15280
|
+
return files.sort();
|
|
15281
|
+
}
|
|
15282
|
+
/**
|
|
15283
|
+
* Selects files changed since a specific commit (incremental mode).
|
|
15284
|
+
* Includes: git diff, modified files, untracked files.
|
|
15285
|
+
*/
|
|
15286
|
+
async selectChangedFiles(sinceCommit, excludes, baseDir) {
|
|
15287
|
+
const changedFiles = /* @__PURE__ */ new Set();
|
|
15288
|
+
try {
|
|
15289
|
+
const diffOutput = execSync(
|
|
15290
|
+
`git diff --name-only ${sinceCommit}..HEAD`,
|
|
15291
|
+
{ cwd: baseDir, encoding: "utf-8" }
|
|
15292
|
+
);
|
|
15293
|
+
diffOutput.split("\n").filter(Boolean).forEach((f) => changedFiles.add(f));
|
|
15294
|
+
const statusOutput = execSync(
|
|
15295
|
+
`git status --porcelain`,
|
|
15296
|
+
{ cwd: baseDir, encoding: "utf-8" }
|
|
15297
|
+
);
|
|
15298
|
+
statusOutput.split("\n").filter(Boolean).forEach((line) => {
|
|
15299
|
+
const file = line.slice(3).trim();
|
|
15300
|
+
if (file) changedFiles.add(file);
|
|
15301
|
+
});
|
|
15302
|
+
const untrackedOutput = execSync(
|
|
15303
|
+
`git ls-files --others --exclude-standard`,
|
|
15304
|
+
{ cwd: baseDir, encoding: "utf-8" }
|
|
15305
|
+
);
|
|
15306
|
+
untrackedOutput.split("\n").filter(Boolean).forEach((f) => changedFiles.add(f));
|
|
15307
|
+
} catch {
|
|
15308
|
+
return [];
|
|
15309
|
+
}
|
|
15310
|
+
const allFiles = Array.from(changedFiles);
|
|
15311
|
+
if (allFiles.length === 0) {
|
|
15312
|
+
return [];
|
|
15313
|
+
}
|
|
15314
|
+
const filtered = await fg(allFiles, {
|
|
15315
|
+
cwd: baseDir,
|
|
15316
|
+
ignore: excludes,
|
|
15317
|
+
onlyFiles: true,
|
|
15318
|
+
absolute: false
|
|
15319
|
+
});
|
|
15320
|
+
return filtered.sort();
|
|
15321
|
+
}
|
|
15322
|
+
/**
|
|
15323
|
+
* Reads .gitignore and converts patterns to glob format.
|
|
15324
|
+
* Returns empty array if .gitignore doesn't exist.
|
|
15325
|
+
*/
|
|
15326
|
+
async loadGitignorePatterns(baseDir) {
|
|
15327
|
+
const gitignorePath = path6.join(baseDir, ".gitignore");
|
|
15328
|
+
try {
|
|
15329
|
+
const content = await fs11.readFile(gitignorePath, "utf-8");
|
|
15330
|
+
return this.parseGitignore(content);
|
|
15331
|
+
} catch {
|
|
15332
|
+
return [];
|
|
15333
|
+
}
|
|
15334
|
+
}
|
|
15335
|
+
/**
|
|
15336
|
+
* Parses .gitignore content into glob patterns.
|
|
15337
|
+
* Handles comments, empty lines, and directory patterns.
|
|
15338
|
+
*/
|
|
15339
|
+
parseGitignore(content) {
|
|
15340
|
+
return content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((pattern) => {
|
|
15341
|
+
if (pattern.endsWith("/")) {
|
|
15342
|
+
return `**/${pattern}**`;
|
|
15343
|
+
}
|
|
15344
|
+
if (!pattern.includes("/")) {
|
|
15345
|
+
return `**/${pattern}`;
|
|
15346
|
+
}
|
|
15347
|
+
return pattern;
|
|
15348
|
+
});
|
|
15349
|
+
}
|
|
15350
|
+
};
|
|
15351
|
+
|
|
15352
|
+
// src/source_auditor/scoring_engine.ts
|
|
15353
|
+
var ScoringEngine = class {
|
|
15354
|
+
/**
|
|
15355
|
+
* Applies scoring rules to findings.
|
|
15356
|
+
* Currently returns findings unchanged (future enhancement).
|
|
15357
|
+
* @param findings - Findings to score
|
|
15358
|
+
* @returns Scored findings (same as input for now)
|
|
15359
|
+
*/
|
|
15360
|
+
score(findings) {
|
|
15361
|
+
return findings;
|
|
15362
|
+
}
|
|
15363
|
+
};
|
|
15364
|
+
|
|
15365
|
+
// src/source_auditor/source_auditor.ts
|
|
15366
|
+
var BATCH_SIZE = 100;
|
|
15367
|
+
var SourceAuditorModule = class {
|
|
15368
|
+
/**
|
|
15369
|
+
* Creates module instance with injected dependencies.
|
|
15370
|
+
* ScopeSelector and ScoringEngine are internal components.
|
|
15371
|
+
*/
|
|
15372
|
+
constructor(deps) {
|
|
15373
|
+
this.deps = deps;
|
|
15374
|
+
this.scopeSelector = new ScopeSelector();
|
|
15375
|
+
this.scoringEngine = new ScoringEngine();
|
|
15376
|
+
}
|
|
15377
|
+
scopeSelector;
|
|
15378
|
+
scoringEngine;
|
|
15379
|
+
/**
|
|
15380
|
+
* Executes complete source code audit.
|
|
15381
|
+
* Pipeline: Scope -> Detect -> Filter -> Score -> Output
|
|
15382
|
+
*/
|
|
15383
|
+
async audit(options) {
|
|
15384
|
+
const startTime = Date.now();
|
|
15385
|
+
const baseDir = options.baseDir || process.cwd();
|
|
15386
|
+
const files = await this.scopeSelector.selectFiles(options.scope, baseDir);
|
|
15387
|
+
if (files.length === 0) {
|
|
15388
|
+
return this.createEmptyResult(startTime);
|
|
15389
|
+
}
|
|
15390
|
+
let waivers = [];
|
|
15391
|
+
try {
|
|
15392
|
+
waivers = await this.deps.waiverReader.loadActiveWaivers();
|
|
15393
|
+
} catch {
|
|
15394
|
+
}
|
|
15395
|
+
const { findings, scannedLines, detectors } = await this.runDetection(
|
|
15396
|
+
files,
|
|
15397
|
+
baseDir
|
|
15398
|
+
);
|
|
15399
|
+
const { newFindings, acknowledgedCount } = this.filterByWaivers(
|
|
15400
|
+
findings,
|
|
15401
|
+
waivers
|
|
15402
|
+
);
|
|
15403
|
+
const scoredFindings = this.scoringEngine.score(newFindings);
|
|
15404
|
+
const duration = Date.now() - startTime;
|
|
15405
|
+
return {
|
|
15406
|
+
findings: scoredFindings,
|
|
15407
|
+
summary: this.calculateSummary(scoredFindings),
|
|
15408
|
+
scannedFiles: files.length,
|
|
15409
|
+
scannedLines,
|
|
15410
|
+
duration,
|
|
15411
|
+
detectors: [...new Set(detectors)],
|
|
15412
|
+
waivers: {
|
|
15413
|
+
acknowledged: acknowledgedCount,
|
|
15414
|
+
new: scoredFindings.length
|
|
15415
|
+
}
|
|
15416
|
+
};
|
|
15417
|
+
}
|
|
15418
|
+
/**
|
|
15419
|
+
* Runs detection on all files, processing in batches for large file counts.
|
|
15420
|
+
*/
|
|
15421
|
+
async runDetection(files, baseDir) {
|
|
15422
|
+
const allFindings = [];
|
|
15423
|
+
const detectors = [];
|
|
15424
|
+
let scannedLines = 0;
|
|
15425
|
+
const batches = this.createBatches(files, files.length > 1e3 ? BATCH_SIZE : files.length);
|
|
15426
|
+
for (const batch of batches) {
|
|
15427
|
+
for (const file of batch) {
|
|
15428
|
+
const filePath = path6.join(baseDir, file);
|
|
15429
|
+
try {
|
|
15430
|
+
const content = await fs11.readFile(filePath, "utf-8");
|
|
15431
|
+
scannedLines += content.split("\n").length;
|
|
15432
|
+
const fileFindings = await this.deps.piiDetector.detect(content, file);
|
|
15433
|
+
for (const finding of fileFindings) {
|
|
15434
|
+
allFindings.push(finding);
|
|
15435
|
+
if (!detectors.includes(finding.detector)) {
|
|
15436
|
+
detectors.push(finding.detector);
|
|
15437
|
+
}
|
|
15438
|
+
}
|
|
15439
|
+
} catch {
|
|
15440
|
+
continue;
|
|
15441
|
+
}
|
|
15442
|
+
}
|
|
15443
|
+
}
|
|
15444
|
+
return { findings: allFindings, scannedLines, detectors };
|
|
15445
|
+
}
|
|
15446
|
+
/**
|
|
15447
|
+
* Creates batches of files for processing.
|
|
15448
|
+
*/
|
|
15449
|
+
createBatches(items, batchSize) {
|
|
15450
|
+
const batches = [];
|
|
15451
|
+
for (let i = 0; i < items.length; i += batchSize) {
|
|
15452
|
+
batches.push(items.slice(i, i + batchSize));
|
|
15453
|
+
}
|
|
15454
|
+
return batches;
|
|
15455
|
+
}
|
|
15456
|
+
/**
|
|
15457
|
+
* Filters findings that already have active waivers.
|
|
15458
|
+
* @returns new findings and count of acknowledged
|
|
15459
|
+
*/
|
|
15460
|
+
filterByWaivers(findings, waivers) {
|
|
15461
|
+
const waiverFingerprints = new Set(waivers.map((w) => w.fingerprint));
|
|
15462
|
+
const newFindings = findings.filter(
|
|
15463
|
+
(f) => !waiverFingerprints.has(f.fingerprint)
|
|
15464
|
+
);
|
|
15465
|
+
const acknowledgedCount = findings.length - newFindings.length;
|
|
15466
|
+
return { newFindings, acknowledgedCount };
|
|
15467
|
+
}
|
|
15468
|
+
/**
|
|
15469
|
+
* Calculates summary of findings by severity, category, and detector.
|
|
15470
|
+
*/
|
|
15471
|
+
calculateSummary(findings) {
|
|
15472
|
+
const summary = {
|
|
15473
|
+
total: findings.length,
|
|
15474
|
+
bySeverity: { critical: 0, high: 0, medium: 0, low: 0, info: 0 },
|
|
15475
|
+
byCategory: {},
|
|
15476
|
+
byDetector: { regex: 0, heuristic: 0, llm: 0 }
|
|
15477
|
+
};
|
|
15478
|
+
for (const finding of findings) {
|
|
15479
|
+
summary.bySeverity[finding.severity]++;
|
|
15480
|
+
summary.byCategory[finding.category] = (summary.byCategory[finding.category] || 0) + 1;
|
|
15481
|
+
summary.byDetector[finding.detector]++;
|
|
15482
|
+
}
|
|
15483
|
+
return summary;
|
|
15484
|
+
}
|
|
15485
|
+
/**
|
|
15486
|
+
* Creates empty result for when no files are selected.
|
|
15487
|
+
*/
|
|
15488
|
+
createEmptyResult(startTime) {
|
|
15489
|
+
return {
|
|
15490
|
+
findings: [],
|
|
15491
|
+
summary: {
|
|
15492
|
+
total: 0,
|
|
15493
|
+
bySeverity: { critical: 0, high: 0, medium: 0, low: 0, info: 0 },
|
|
15494
|
+
byCategory: {},
|
|
15495
|
+
byDetector: { regex: 0, heuristic: 0, llm: 0 }
|
|
15496
|
+
},
|
|
15497
|
+
scannedFiles: 0,
|
|
15498
|
+
scannedLines: 0,
|
|
15499
|
+
duration: Date.now() - startTime,
|
|
15500
|
+
detectors: [],
|
|
15501
|
+
waivers: { acknowledged: 0, new: 0 }
|
|
15502
|
+
};
|
|
15503
|
+
}
|
|
15504
|
+
};
|
|
15505
|
+
|
|
15506
|
+
// src/source_auditor/waiver_reader.ts
|
|
15507
|
+
var WaiverReader = class {
|
|
15508
|
+
constructor(feedbackAdapter) {
|
|
15509
|
+
this.feedbackAdapter = feedbackAdapter;
|
|
15510
|
+
}
|
|
15511
|
+
/**
|
|
15512
|
+
* Loads all active waivers (non-expired).
|
|
15513
|
+
* Filters by type: "approval" and metadata.fingerprint present.
|
|
15514
|
+
*/
|
|
15515
|
+
async loadActiveWaivers() {
|
|
15516
|
+
const allFeedback = await this.feedbackAdapter.getAllFeedback();
|
|
15517
|
+
const now = /* @__PURE__ */ new Date();
|
|
15518
|
+
const result = [];
|
|
15519
|
+
for (const f of allFeedback) {
|
|
15520
|
+
if (f.type !== "approval" || !f.metadata) continue;
|
|
15521
|
+
const meta = f.metadata;
|
|
15522
|
+
if (typeof meta.fingerprint !== "string") continue;
|
|
15523
|
+
if (meta.expiresAt && new Date(meta.expiresAt) <= now) continue;
|
|
15524
|
+
const waiver = {
|
|
15525
|
+
fingerprint: meta.fingerprint,
|
|
15526
|
+
ruleId: meta.ruleId,
|
|
15527
|
+
feedback: f
|
|
15528
|
+
};
|
|
15529
|
+
if (meta.expiresAt) {
|
|
15530
|
+
waiver.expiresAt = new Date(meta.expiresAt);
|
|
15531
|
+
}
|
|
15532
|
+
result.push(waiver);
|
|
15533
|
+
}
|
|
15534
|
+
return result;
|
|
15535
|
+
}
|
|
15536
|
+
/**
|
|
15537
|
+
* Checks if a specific finding has an active waiver.
|
|
15538
|
+
*/
|
|
15539
|
+
async hasActiveWaiver(fingerprint) {
|
|
15540
|
+
const waivers = await this.loadActiveWaivers();
|
|
15541
|
+
return waivers.some((w) => w.fingerprint === fingerprint);
|
|
15542
|
+
}
|
|
15543
|
+
/**
|
|
15544
|
+
* Gets waivers for a specific ExecutionRecord.
|
|
15545
|
+
*/
|
|
15546
|
+
async getWaiversForExecution(executionId) {
|
|
15547
|
+
const feedback = await this.feedbackAdapter.getFeedbackByEntity(executionId);
|
|
15548
|
+
const now = /* @__PURE__ */ new Date();
|
|
15549
|
+
const result = [];
|
|
15550
|
+
for (const f of feedback) {
|
|
15551
|
+
if (f.type !== "approval" || !f.metadata) continue;
|
|
15552
|
+
const meta = f.metadata;
|
|
15553
|
+
if (typeof meta.fingerprint !== "string") continue;
|
|
15554
|
+
if (meta.expiresAt && new Date(meta.expiresAt) <= now) continue;
|
|
15555
|
+
const waiver = {
|
|
15556
|
+
fingerprint: meta.fingerprint,
|
|
15557
|
+
ruleId: meta.ruleId,
|
|
15558
|
+
feedback: f
|
|
15559
|
+
};
|
|
15560
|
+
if (meta.expiresAt) {
|
|
15561
|
+
waiver.expiresAt = new Date(meta.expiresAt);
|
|
15562
|
+
}
|
|
15563
|
+
result.push(waiver);
|
|
15564
|
+
}
|
|
15565
|
+
return result;
|
|
15566
|
+
}
|
|
15567
|
+
};
|
|
15568
|
+
|
|
15569
|
+
// src/source_auditor/waiver_writer.ts
|
|
15570
|
+
var WaiverWriter = class {
|
|
15571
|
+
constructor(feedbackAdapter) {
|
|
15572
|
+
this.feedbackAdapter = feedbackAdapter;
|
|
15573
|
+
}
|
|
15574
|
+
/**
|
|
15575
|
+
* Creates a waiver for a specific finding.
|
|
15576
|
+
* The waiver is stored as FeedbackRecord with type: "approval".
|
|
15577
|
+
*/
|
|
15578
|
+
async createWaiver(options, actorId) {
|
|
15579
|
+
const { finding, executionId, justification, expiresAt, relatedTaskId } = options;
|
|
15580
|
+
const metadata = {
|
|
15581
|
+
fingerprint: finding.fingerprint,
|
|
15582
|
+
ruleId: finding.ruleId,
|
|
15583
|
+
file: finding.file,
|
|
15584
|
+
line: finding.line
|
|
15585
|
+
};
|
|
15586
|
+
if (expiresAt) {
|
|
15587
|
+
metadata.expiresAt = expiresAt;
|
|
15588
|
+
}
|
|
15589
|
+
if (relatedTaskId) {
|
|
15590
|
+
metadata.relatedTaskId = relatedTaskId;
|
|
15591
|
+
}
|
|
15592
|
+
await this.feedbackAdapter.create(
|
|
15593
|
+
{
|
|
15594
|
+
entityType: "execution",
|
|
15595
|
+
entityId: executionId,
|
|
15596
|
+
type: "approval",
|
|
15597
|
+
status: "resolved",
|
|
15598
|
+
content: justification,
|
|
15599
|
+
metadata
|
|
15600
|
+
},
|
|
15601
|
+
actorId
|
|
15602
|
+
);
|
|
15603
|
+
}
|
|
15604
|
+
/**
|
|
15605
|
+
* Creates waivers in batch for multiple findings.
|
|
15606
|
+
*/
|
|
15607
|
+
async createWaiversBatch(findings, executionId, justification, actorId) {
|
|
15608
|
+
for (const finding of findings) {
|
|
15609
|
+
await this.createWaiver(
|
|
15610
|
+
{
|
|
15611
|
+
finding,
|
|
15612
|
+
executionId,
|
|
15613
|
+
justification
|
|
15614
|
+
},
|
|
15615
|
+
actorId
|
|
15616
|
+
);
|
|
15617
|
+
}
|
|
15618
|
+
}
|
|
15619
|
+
};
|
|
15620
|
+
|
|
15621
|
+
export { adapters_exports as Adapters, backlog_adapter_exports as BacklogAdapter, changelog_adapter_exports as ChangelogAdapter, config_manager_exports as Config, crypto_exports as Crypto, diagram_generator_exports as DiagramGenerator, event_bus_exports as EventBus, execution_adapter_exports as ExecutionAdapter, factories_exports as Factories, feedback_adapter_exports as FeedbackAdapter, git_exports as Git, identity_adapter_exports as IdentityAdapter, indexer_adapter_exports as IndexerAdapter, lint_exports as Lint, logger_exports as Logger, metrics_adapter_exports as MetricsAdapter, pii_detector_exports as PiiDetector, project_adapter_exports as ProjectAdapter, types_exports as Records, schemas_exports as Schemas, source_auditor_exports as SourceAuditor, store_exports as Store, sync_exports as Sync, validation_exports as Validation, workflow_methodology_adapter_exports as WorkflowMethodologyAdapter };
|
|
14763
15622
|
//# sourceMappingURL=index.js.map
|
|
14764
15623
|
//# sourceMappingURL=index.js.map
|