bilt-toolkit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +0 -0
- package/README.md +168 -0
- package/bin/bilt.js +2 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +161 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/doctor.d.ts +13 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +194 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/fix.d.ts +12 -0
- package/dist/commands/fix.d.ts.map +1 -0
- package/dist/commands/fix.js +273 -0
- package/dist/commands/fix.js.map +1 -0
- package/dist/commands/init.d.ts +11 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +101 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/scan.d.ts +17 -0
- package/dist/commands/scan.d.ts.map +1 -0
- package/dist/commands/scan.js +267 -0
- package/dist/commands/scan.js.map +1 -0
- package/dist/commands/undo.d.ts +12 -0
- package/dist/commands/undo.d.ts.map +1 -0
- package/dist/commands/undo.js +90 -0
- package/dist/commands/undo.js.map +1 -0
- package/dist/commands/watch.d.ts +10 -0
- package/dist/commands/watch.d.ts.map +1 -0
- package/dist/commands/watch.js +68 -0
- package/dist/commands/watch.js.map +1 -0
- package/dist/config/config.d.ts +8 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +75 -0
- package/dist/config/config.js.map +1 -0
- package/dist/core/fix/confirm.d.ts +21 -0
- package/dist/core/fix/confirm.d.ts.map +1 -0
- package/dist/core/fix/confirm.js +60 -0
- package/dist/core/fix/confirm.js.map +1 -0
- package/dist/core/fix/env-fix.d.ts +40 -0
- package/dist/core/fix/env-fix.d.ts.map +1 -0
- package/dist/core/fix/env-fix.js +186 -0
- package/dist/core/fix/env-fix.js.map +1 -0
- package/dist/core/fix/snapshot.d.ts +30 -0
- package/dist/core/fix/snapshot.d.ts.map +1 -0
- package/dist/core/fix/snapshot.js +186 -0
- package/dist/core/fix/snapshot.js.map +1 -0
- package/dist/core/rules/entropy.d.ts +29 -0
- package/dist/core/rules/entropy.d.ts.map +1 -0
- package/dist/core/rules/entropy.js +117 -0
- package/dist/core/rules/entropy.js.map +1 -0
- package/dist/core/rules/providers.d.ts +12 -0
- package/dist/core/rules/providers.d.ts.map +1 -0
- package/dist/core/rules/providers.js +184 -0
- package/dist/core/rules/providers.js.map +1 -0
- package/dist/core/rules/secret-rules.d.ts +3 -0
- package/dist/core/rules/secret-rules.d.ts.map +1 -0
- package/dist/core/rules/secret-rules.js +205 -0
- package/dist/core/rules/secret-rules.js.map +1 -0
- package/dist/core/scan/env.d.ts +47 -0
- package/dist/core/scan/env.d.ts.map +1 -0
- package/dist/core/scan/env.js +301 -0
- package/dist/core/scan/env.js.map +1 -0
- package/dist/core/scan/framework.d.ts +26 -0
- package/dist/core/scan/framework.d.ts.map +1 -0
- package/dist/core/scan/framework.js +254 -0
- package/dist/core/scan/framework.js.map +1 -0
- package/dist/core/scan/gitignore.d.ts +20 -0
- package/dist/core/scan/gitignore.d.ts.map +1 -0
- package/dist/core/scan/gitignore.js +217 -0
- package/dist/core/scan/gitignore.js.map +1 -0
- package/dist/core/scan/secrets.d.ts +27 -0
- package/dist/core/scan/secrets.d.ts.map +1 -0
- package/dist/core/scan/secrets.js +247 -0
- package/dist/core/scan/secrets.js.map +1 -0
- package/dist/core/score/health.d.ts +29 -0
- package/dist/core/score/health.d.ts.map +1 -0
- package/dist/core/score/health.js +73 -0
- package/dist/core/score/health.js.map +1 -0
- package/dist/core/watch/watcher.d.ts +29 -0
- package/dist/core/watch/watcher.d.ts.map +1 -0
- package/dist/core/watch/watcher.js +176 -0
- package/dist/core/watch/watcher.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/plugins/interface.d.ts +12 -0
- package/dist/plugins/interface.d.ts.map +1 -0
- package/dist/plugins/interface.js +91 -0
- package/dist/plugins/interface.js.map +1 -0
- package/dist/plugins/loader.d.ts +10 -0
- package/dist/plugins/loader.d.ts.map +1 -0
- package/dist/plugins/loader.js +104 -0
- package/dist/plugins/loader.js.map +1 -0
- package/dist/plugins/official/docker.d.ts +4 -0
- package/dist/plugins/official/docker.d.ts.map +1 -0
- package/dist/plugins/official/docker.js +111 -0
- package/dist/plugins/official/docker.js.map +1 -0
- package/dist/plugins/official/terraform.d.ts +4 -0
- package/dist/plugins/official/terraform.d.ts.map +1 -0
- package/dist/plugins/official/terraform.js +131 -0
- package/dist/plugins/official/terraform.js.map +1 -0
- package/dist/types/index.d.ts +158 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/ui/format.d.ts +43 -0
- package/dist/ui/format.d.ts.map +1 -0
- package/dist/ui/format.js +207 -0
- package/dist/ui/format.js.map +1 -0
- package/dist/ui/reporter.d.ts +39 -0
- package/dist/ui/reporter.d.ts.map +1 -0
- package/dist/ui/reporter.js +254 -0
- package/dist/ui/reporter.js.map +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// ─── Gitignore Analysis ──────────────────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Checks whether sensitive .env files are properly covered by .gitignore.
|
|
4
|
+
// Uses simple-git for accurate `git check-ignore` verification when
|
|
5
|
+
// running inside a git repository.
|
|
6
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
import { simpleGit } from "simple-git";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import fs from "node:fs/promises";
|
|
10
|
+
import fsSync from "node:fs";
|
|
11
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
12
|
+
let findingCounter = 0;
|
|
13
|
+
function nextId(prefix) {
|
|
14
|
+
return `${prefix}-${Date.now()}-${++findingCounter}`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Basename extraction that handles both `/` and `\` separators.
|
|
18
|
+
*/
|
|
19
|
+
function basename(filePath) {
|
|
20
|
+
return filePath.split(/[\\/]/).pop() ?? filePath;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* File names that are intentionally tracked (they contain no real secrets).
|
|
24
|
+
*/
|
|
25
|
+
const TEMPLATE_NAMES = new Set([
|
|
26
|
+
".env.example",
|
|
27
|
+
".env.template",
|
|
28
|
+
".env.sample",
|
|
29
|
+
]);
|
|
30
|
+
/**
|
|
31
|
+
* Determine if a file is a template / example that SHOULD be tracked.
|
|
32
|
+
*/
|
|
33
|
+
function isTemplateFile(filePath) {
|
|
34
|
+
return TEMPLATE_NAMES.has(basename(filePath).toLowerCase());
|
|
35
|
+
}
|
|
36
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
37
|
+
/**
|
|
38
|
+
* Parse a `.gitignore` file's content into a list of non-empty,
|
|
39
|
+
* non-comment patterns.
|
|
40
|
+
*/
|
|
41
|
+
export function parseGitignore(content) {
|
|
42
|
+
return content
|
|
43
|
+
.split("\n")
|
|
44
|
+
.map((line) => line.trim())
|
|
45
|
+
.filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Check whether the given env files are properly ignored by git.
|
|
49
|
+
*
|
|
50
|
+
* Strategy:
|
|
51
|
+
* 1. For each env file, skip if it's a template (should be tracked).
|
|
52
|
+
* 2. Fall back to simple pattern matching against gitignore content.
|
|
53
|
+
*/
|
|
54
|
+
export function checkEnvFilesIgnored(projectDir, envFiles) {
|
|
55
|
+
let gitignoreContent = "";
|
|
56
|
+
try {
|
|
57
|
+
gitignoreContent = fsSync.readFileSync(path.join(projectDir, ".gitignore"), "utf-8");
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Missing gitignore
|
|
61
|
+
}
|
|
62
|
+
const findings = [];
|
|
63
|
+
const patterns = parseGitignore(gitignoreContent);
|
|
64
|
+
for (const envFile of envFiles) {
|
|
65
|
+
// Skip template files — they should be tracked
|
|
66
|
+
if (isTemplateFile(envFile))
|
|
67
|
+
continue;
|
|
68
|
+
const name = basename(envFile);
|
|
69
|
+
const relativePath = path.relative(projectDir, envFile).replace(/\\/g, "/");
|
|
70
|
+
// Check if any gitignore pattern covers this file
|
|
71
|
+
const isCovered = patternCoversFile(patterns, name, relativePath);
|
|
72
|
+
if (!isCovered) {
|
|
73
|
+
findings.push({
|
|
74
|
+
id: nextId("gitignore-missing"),
|
|
75
|
+
severity: "critical",
|
|
76
|
+
category: "gitignore-missing",
|
|
77
|
+
message: `${name} is not ignored by git — secrets may be committed`,
|
|
78
|
+
file: envFile,
|
|
79
|
+
suggestion: `Add "${name}" or ".env*" to your .gitignore file`,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return findings;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Enhanced check using `git check-ignore` for accurate results.
|
|
87
|
+
* Falls back to pattern matching if git is not available.
|
|
88
|
+
*/
|
|
89
|
+
export async function checkEnvFilesIgnoredWithGit(projectDir, envFiles) {
|
|
90
|
+
const findings = [];
|
|
91
|
+
// Try to use git check-ignore for accuracy
|
|
92
|
+
let useGit = false;
|
|
93
|
+
const git = simpleGit(projectDir);
|
|
94
|
+
git.env({
|
|
95
|
+
...process.env,
|
|
96
|
+
GIT_DIR: undefined,
|
|
97
|
+
GIT_WORK_TREE: undefined,
|
|
98
|
+
});
|
|
99
|
+
try {
|
|
100
|
+
const isRepo = await git.checkIsRepo();
|
|
101
|
+
if (isRepo)
|
|
102
|
+
useGit = true;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Git not available or not a repo — fall back to pattern matching
|
|
106
|
+
}
|
|
107
|
+
let gitignoreContent = "";
|
|
108
|
+
try {
|
|
109
|
+
gitignoreContent = await fs.readFile(path.join(projectDir, ".gitignore"), "utf-8");
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// Missing gitignore
|
|
113
|
+
}
|
|
114
|
+
for (const envFile of envFiles) {
|
|
115
|
+
if (isTemplateFile(envFile))
|
|
116
|
+
continue;
|
|
117
|
+
const name = basename(envFile);
|
|
118
|
+
let isIgnored = false;
|
|
119
|
+
if (useGit) {
|
|
120
|
+
try {
|
|
121
|
+
await git.raw(["check-ignore", "-q", envFile]);
|
|
122
|
+
// Exit code 0 means the file IS ignored
|
|
123
|
+
isIgnored = true;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// Exit code 1 means the file is NOT ignored
|
|
127
|
+
// (simple-git throws on non-zero exit)
|
|
128
|
+
isIgnored = false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
// Fall back to pattern analysis
|
|
133
|
+
const patterns = parseGitignore(gitignoreContent);
|
|
134
|
+
const relativePath = path
|
|
135
|
+
.relative(projectDir, envFile)
|
|
136
|
+
.replace(/\\/g, "/");
|
|
137
|
+
isIgnored = patternCoversFile(patterns, name, relativePath);
|
|
138
|
+
}
|
|
139
|
+
if (!isIgnored) {
|
|
140
|
+
findings.push({
|
|
141
|
+
id: nextId("gitignore-missing"),
|
|
142
|
+
severity: "critical",
|
|
143
|
+
category: "gitignore-missing",
|
|
144
|
+
message: `${name} is not ignored by git — secrets may be committed`,
|
|
145
|
+
file: envFile,
|
|
146
|
+
suggestion: `Add "${name}" or ".env*" to your .gitignore file`,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return findings;
|
|
151
|
+
}
|
|
152
|
+
// ─── Pattern Matching (fallback) ─────────────────────────────────────────────
|
|
153
|
+
/**
|
|
154
|
+
* Check whether a given file is covered by any of the parsed gitignore
|
|
155
|
+
* patterns. This is a simplified matcher — for accurate results use
|
|
156
|
+
* `git check-ignore` via `checkEnvFilesIgnoredWithGit`.
|
|
157
|
+
*
|
|
158
|
+
* Supports:
|
|
159
|
+
* • Exact name match (`.env`)
|
|
160
|
+
* • Glob star patterns (`.env.*`, `.env*`)
|
|
161
|
+
* • Path-relative patterns (`config/.env`)
|
|
162
|
+
* • Negation patterns (`!.env.example`) — will un-ignore a file
|
|
163
|
+
*/
|
|
164
|
+
function patternCoversFile(patterns, fileName, relativePath) {
|
|
165
|
+
let covered = false;
|
|
166
|
+
for (const raw of patterns) {
|
|
167
|
+
const negated = raw.startsWith("!");
|
|
168
|
+
const pattern = negated ? raw.slice(1) : raw;
|
|
169
|
+
const matches = patternMatchesSingle(pattern, fileName, relativePath);
|
|
170
|
+
if (matches) {
|
|
171
|
+
covered = !negated;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return covered;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Simple glob-like matching for a single gitignore pattern.
|
|
178
|
+
*/
|
|
179
|
+
function patternMatchesSingle(pattern, fileName, relativePath) {
|
|
180
|
+
// Trim trailing slashes (directory markers) for file matching
|
|
181
|
+
const p = pattern.replace(/\/$/, "");
|
|
182
|
+
// Exact filename match
|
|
183
|
+
if (p === fileName)
|
|
184
|
+
return true;
|
|
185
|
+
// Path match
|
|
186
|
+
if (p === relativePath)
|
|
187
|
+
return true;
|
|
188
|
+
// Pattern ends with `*` — prefix match
|
|
189
|
+
if (p.endsWith("*")) {
|
|
190
|
+
const prefix = p.slice(0, -1);
|
|
191
|
+
if (fileName.startsWith(prefix))
|
|
192
|
+
return true;
|
|
193
|
+
if (relativePath.startsWith(prefix))
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
// Pattern contains `.*` — e.g. `.env.*` matches `.env.local`
|
|
197
|
+
if (p.includes(".*")) {
|
|
198
|
+
const re = new RegExp("^" +
|
|
199
|
+
p.replace(/\./g, "\\.").replace(/\*/g, "[^/]*").replace(/\?/g, ".") +
|
|
200
|
+
"$");
|
|
201
|
+
if (re.test(fileName) || re.test(relativePath))
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
// Pattern with `**` — recursive match
|
|
205
|
+
if (p.includes("**")) {
|
|
206
|
+
const re = new RegExp("^" +
|
|
207
|
+
p
|
|
208
|
+
.replace(/\./g, "\\.")
|
|
209
|
+
.replace(/\*\*/g, ".*")
|
|
210
|
+
.replace(/(?<!\.)(\*)/g, "[^/]*") +
|
|
211
|
+
"$");
|
|
212
|
+
if (re.test(relativePath))
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
//# sourceMappingURL=gitignore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gitignore.js","sourceRoot":"","sources":["../../../src/core/scan/gitignore.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { SecretRule, ScanFinding, BiltConfig } from "../../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Scan file content for secrets using the specified rules.
|
|
4
|
+
*
|
|
5
|
+
* Before reporting a match, the function filters out:
|
|
6
|
+
* 1. Placeholder / dummy-value heuristics
|
|
7
|
+
* 2. Entropy threshold (for the `generic-high-entropy` rule the
|
|
8
|
+
* captured group is used; for others the full match is checked)
|
|
9
|
+
*
|
|
10
|
+
* Each surviving match is turned into a `ScanFinding` with:
|
|
11
|
+
* • Line number within the file
|
|
12
|
+
* • Masked preview of the matched value
|
|
13
|
+
* • Provider info (if determinable)
|
|
14
|
+
*/
|
|
15
|
+
export declare function scanFileForSecrets(content: string, filePath: string, rulesOrConfig?: SecretRule[] | BiltConfig, entropyThreshold?: number): ScanFinding[];
|
|
16
|
+
/**
|
|
17
|
+
* Scan the recent git history for secrets that may have been committed.
|
|
18
|
+
*
|
|
19
|
+
* Uses `simple-git` to iterate over the last `depth` commits (default
|
|
20
|
+
* 10), extract the diff for each, and run the same secret-scanning
|
|
21
|
+
* logic used for file content.
|
|
22
|
+
*
|
|
23
|
+
* Findings include the commit SHA in their `file` field so the user
|
|
24
|
+
* knows which commit to rewrite / clean.
|
|
25
|
+
*/
|
|
26
|
+
export declare function scanGitHistory(repoPath: string, rules: SecretRule[], entropyThreshold: number, depth?: number): Promise<ScanFinding[]>;
|
|
27
|
+
//# sourceMappingURL=secrets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secrets.d.ts","sourceRoot":"","sources":["../../../src/core/scan/secrets.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAwEhF;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,aAAa,GAAE,UAAU,EAAE,GAAG,UAAyB,EACvD,gBAAgB,GAAE,MAAY,GAC7B,WAAW,EAAE,CAkGf;AAED;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,UAAU,EAAE,EACnB,gBAAgB,EAAE,MAAM,EACxB,KAAK,SAAK,GACT,OAAO,CAAC,WAAW,EAAE,CAAC,CAqExB"}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// ─── Secret Scanner ──────────────────────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Scans file content (and optionally git history) for leaked secrets by
|
|
4
|
+
// running every SecretRule regex and then cross-checking with entropy
|
|
5
|
+
// analysis. Produces ScanFinding objects with masked previews, line
|
|
6
|
+
// numbers, and provider information.
|
|
7
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
8
|
+
import { simpleGit } from "simple-git";
|
|
9
|
+
import { isHighEntropy } from "../rules/entropy.js";
|
|
10
|
+
import { detectProvider } from "../rules/providers.js";
|
|
11
|
+
import { SECRET_RULES } from "../rules/secret-rules.js";
|
|
12
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
13
|
+
let findingCounter = 0;
|
|
14
|
+
function nextId(prefix) {
|
|
15
|
+
return `${prefix}-${Date.now()}-${++findingCounter}`;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Common placeholder / dummy values that should never be flagged as
|
|
19
|
+
* actual secrets.
|
|
20
|
+
*/
|
|
21
|
+
const PLACEHOLDER_PATTERNS = [
|
|
22
|
+
/^<.*>$/, // <YOUR_KEY>, <api-key>
|
|
23
|
+
/^your[_-]?/i, // your-api-key-here, your_key
|
|
24
|
+
/^xxx+$/i, // xxx, xxxx, etc.
|
|
25
|
+
/^change[_-]?me$/i, // CHANGE_ME, changeme
|
|
26
|
+
/^replace[_-]?me$/i, // REPLACE_ME, replaceme
|
|
27
|
+
/^todo$/i, // TODO
|
|
28
|
+
/^fixme$/i, // FIXME
|
|
29
|
+
/^placeholder$/i, // placeholder
|
|
30
|
+
/^example$/i, // example
|
|
31
|
+
/^test$/i, // test
|
|
32
|
+
/^none$/i, // none
|
|
33
|
+
/^null$/i, // null
|
|
34
|
+
/^undefined$/i, // undefined
|
|
35
|
+
/^insert[_-]?/i, // insert-your-key, insert_key_here
|
|
36
|
+
/^dummy/i, // dummy, dummy-key
|
|
37
|
+
/^\*+$/, // ***, ******, etc.
|
|
38
|
+
/^\.{3,}$/, // ..., ......
|
|
39
|
+
/^_+$/, // ___, ______
|
|
40
|
+
/^0+$/, // 000, 0000000
|
|
41
|
+
];
|
|
42
|
+
/**
|
|
43
|
+
* Returns true if the value looks like a placeholder rather than a
|
|
44
|
+
* real secret.
|
|
45
|
+
*/
|
|
46
|
+
function isPlaceholder(value) {
|
|
47
|
+
const trimmed = value.trim();
|
|
48
|
+
if (trimmed.length === 0)
|
|
49
|
+
return true;
|
|
50
|
+
return PLACEHOLDER_PATTERNS.some((re) => re.test(trimmed));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Mask a secret value for safe display: show first 4 and last 4 chars
|
|
54
|
+
* with stars in between. Very short values are fully masked.
|
|
55
|
+
*/
|
|
56
|
+
function maskValue(value) {
|
|
57
|
+
if (value.length <= 8)
|
|
58
|
+
return "*".repeat(value.length);
|
|
59
|
+
const start = value.slice(0, 4);
|
|
60
|
+
const end = value.slice(-4);
|
|
61
|
+
return `${start}${"*".repeat(Math.min(value.length - 8, 20))}${end}`;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Get the 1-indexed line number within `content` for a given character
|
|
65
|
+
* offset.
|
|
66
|
+
*/
|
|
67
|
+
function lineNumberAt(content, offset) {
|
|
68
|
+
let line = 1;
|
|
69
|
+
for (let i = 0; i < offset && i < content.length; i++) {
|
|
70
|
+
if (content[i] === "\n")
|
|
71
|
+
line++;
|
|
72
|
+
}
|
|
73
|
+
return line;
|
|
74
|
+
}
|
|
75
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
76
|
+
/**
|
|
77
|
+
* Scan file content for secrets using the specified rules.
|
|
78
|
+
*
|
|
79
|
+
* Before reporting a match, the function filters out:
|
|
80
|
+
* 1. Placeholder / dummy-value heuristics
|
|
81
|
+
* 2. Entropy threshold (for the `generic-high-entropy` rule the
|
|
82
|
+
* captured group is used; for others the full match is checked)
|
|
83
|
+
*
|
|
84
|
+
* Each surviving match is turned into a `ScanFinding` with:
|
|
85
|
+
* • Line number within the file
|
|
86
|
+
* • Masked preview of the matched value
|
|
87
|
+
* • Provider info (if determinable)
|
|
88
|
+
*/
|
|
89
|
+
export function scanFileForSecrets(content, filePath, rulesOrConfig = SECRET_RULES, entropyThreshold = 4.5) {
|
|
90
|
+
let rules;
|
|
91
|
+
let threshold = entropyThreshold;
|
|
92
|
+
if (Array.isArray(rulesOrConfig)) {
|
|
93
|
+
rules = rulesOrConfig;
|
|
94
|
+
}
|
|
95
|
+
else if (rulesOrConfig && typeof rulesOrConfig === "object") {
|
|
96
|
+
// It's a BiltConfig
|
|
97
|
+
const config = rulesOrConfig;
|
|
98
|
+
rules = [...SECRET_RULES, ...(config.customRules || [])];
|
|
99
|
+
threshold = config.entropyThreshold ?? entropyThreshold;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
rules = SECRET_RULES;
|
|
103
|
+
}
|
|
104
|
+
const findings = [];
|
|
105
|
+
// Split content into lines for ignore-comment checks
|
|
106
|
+
const fileLines = content.split("\n");
|
|
107
|
+
// Track matches we've already reported to avoid duplicates when
|
|
108
|
+
// multiple rules match the same span.
|
|
109
|
+
const seen = new Set();
|
|
110
|
+
for (const rule of rules) {
|
|
111
|
+
// Reset the regex's lastIndex (rules use /g flag)
|
|
112
|
+
rule.pattern.lastIndex = 0;
|
|
113
|
+
let match;
|
|
114
|
+
while ((match = rule.pattern.exec(content)) !== null) {
|
|
115
|
+
// For the generic high-entropy rule, prefer the captured group
|
|
116
|
+
const matchedValue = match[1] ?? match[0];
|
|
117
|
+
// De-duplicate by position + rule
|
|
118
|
+
const dedupeKey = `${rule.id}:${match.index}`;
|
|
119
|
+
if (seen.has(dedupeKey))
|
|
120
|
+
continue;
|
|
121
|
+
seen.add(dedupeKey);
|
|
122
|
+
// Skip placeholders
|
|
123
|
+
if (isPlaceholder(matchedValue))
|
|
124
|
+
continue;
|
|
125
|
+
// For the generic high-entropy rule, require high entropy
|
|
126
|
+
if (rule.id === "generic-high-entropy") {
|
|
127
|
+
if (!isHighEntropy(matchedValue, threshold))
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
// For the AWS secret key rule, require high entropy to avoid
|
|
131
|
+
// false positives from 40-char strings in code.
|
|
132
|
+
if (rule.id === "aws-secret-key") {
|
|
133
|
+
if (!isHighEntropy(matchedValue, threshold))
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const line = lineNumberAt(content, match.index);
|
|
137
|
+
// Check for known public assignments (e.g. ANON_KEY, PUBLISHABLE_KEY)
|
|
138
|
+
const matchedLine = fileLines[line - 1];
|
|
139
|
+
if (matchedLine) {
|
|
140
|
+
const isPublicAssignment = /^[a-z0-9_]*(anon_key|publishable_key|public_key|app_id)[a-z0-9_]*\s*=/i.test(matchedLine.trim());
|
|
141
|
+
if (isPublicAssignment)
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
// Check for inline ignore comments (e.g. gitleaks:allow or bilt:allow)
|
|
145
|
+
const previousLine = line > 1 ? fileLines[line - 2] : undefined;
|
|
146
|
+
const isAllowed = (matchedLine &&
|
|
147
|
+
(matchedLine.includes("gitleaks:allow") ||
|
|
148
|
+
matchedLine.includes("bilt:allow"))) ||
|
|
149
|
+
(previousLine &&
|
|
150
|
+
(previousLine.includes("gitleaks:allow") ||
|
|
151
|
+
previousLine.includes("bilt:allow")));
|
|
152
|
+
if (isAllowed)
|
|
153
|
+
continue;
|
|
154
|
+
const provider = detectProvider(matchedValue, rule.id) ?? undefined;
|
|
155
|
+
findings.push({
|
|
156
|
+
id: nextId("secret"),
|
|
157
|
+
severity: rule.severity,
|
|
158
|
+
category: "secret-detected",
|
|
159
|
+
message: `${rule.name} detected` +
|
|
160
|
+
(provider ? ` (${provider.displayName})` : ""),
|
|
161
|
+
file: filePath,
|
|
162
|
+
line,
|
|
163
|
+
suggestion: provider
|
|
164
|
+
? `Rotate this key at ${provider.rotationUrl} and move the value to a .env file`
|
|
165
|
+
: "Move this value to a .env file and add the file to .gitignore",
|
|
166
|
+
provider,
|
|
167
|
+
ruleId: rule.id,
|
|
168
|
+
preview: maskValue(matchedValue),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return findings;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Scan the recent git history for secrets that may have been committed.
|
|
176
|
+
*
|
|
177
|
+
* Uses `simple-git` to iterate over the last `depth` commits (default
|
|
178
|
+
* 10), extract the diff for each, and run the same secret-scanning
|
|
179
|
+
* logic used for file content.
|
|
180
|
+
*
|
|
181
|
+
* Findings include the commit SHA in their `file` field so the user
|
|
182
|
+
* knows which commit to rewrite / clean.
|
|
183
|
+
*/
|
|
184
|
+
export async function scanGitHistory(repoPath, rules, entropyThreshold, depth = 10) {
|
|
185
|
+
const findings = [];
|
|
186
|
+
const git = simpleGit(repoPath);
|
|
187
|
+
git.env({
|
|
188
|
+
...process.env,
|
|
189
|
+
GIT_DIR: undefined,
|
|
190
|
+
GIT_WORK_TREE: undefined,
|
|
191
|
+
});
|
|
192
|
+
// Verify we're in a git repo
|
|
193
|
+
try {
|
|
194
|
+
const isRepo = await git.checkIsRepo();
|
|
195
|
+
if (!isRepo)
|
|
196
|
+
return findings;
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return findings;
|
|
200
|
+
}
|
|
201
|
+
// Get the last N commits
|
|
202
|
+
let log;
|
|
203
|
+
try {
|
|
204
|
+
log = await git.log({ maxCount: depth });
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
// Repository might have no commits yet
|
|
208
|
+
return findings;
|
|
209
|
+
}
|
|
210
|
+
for (const commit of log.all) {
|
|
211
|
+
let diff;
|
|
212
|
+
try {
|
|
213
|
+
// Get the full diff for this commit
|
|
214
|
+
diff = await git.diff([`${commit.hash}~1`, commit.hash]);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
// First commit has no parent — diff against empty tree
|
|
218
|
+
try {
|
|
219
|
+
diff = await git.diff([
|
|
220
|
+
"4b825dc642cb6eb9a060e54bf899d69f82ef7b21",
|
|
221
|
+
commit.hash,
|
|
222
|
+
]);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// Extract only the added lines (lines starting with `+` in the diff,
|
|
229
|
+
// excluding the `+++` file header).
|
|
230
|
+
const addedLines = diff
|
|
231
|
+
.split("\n")
|
|
232
|
+
.filter((line) => line.startsWith("+") && !line.startsWith("+++"))
|
|
233
|
+
.map((line) => line.slice(1))
|
|
234
|
+
.join("\n");
|
|
235
|
+
if (addedLines.length === 0)
|
|
236
|
+
continue;
|
|
237
|
+
// Scan the added content
|
|
238
|
+
const commitFindings = scanFileForSecrets(addedLines, `git:${commit.hash.slice(0, 8)}`, rules, entropyThreshold);
|
|
239
|
+
// Annotate findings with commit info
|
|
240
|
+
for (const finding of commitFindings) {
|
|
241
|
+
finding.message += ` (found in commit ${commit.hash.slice(0, 8)}: ${commit.message.slice(0, 60)})`;
|
|
242
|
+
findings.push(finding);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return findings;
|
|
246
|
+
}
|
|
247
|
+
//# sourceMappingURL=secrets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secrets.js","sourceRoot":"","sources":["../../../src/core/scan/secrets.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,EAAE;AACF,wEAAwE;AACxE,sEAAsE;AACtE,qEAAqE;AACrE,qCAAqC;AACrC,gFAAgF;AAEhF,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAGxD,gFAAgF;AAEhF,IAAI,cAAc,GAAG,CAAC,CAAC;AAEvB,SAAS,MAAM,CAAC,MAAc;IAC5B,OAAO,GAAG,MAAM,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC;AACvD,CAAC;AAED;;;GAGG;AACH,MAAM,oBAAoB,GAAa;IACrC,QAAQ,EAAE,wBAAwB;IAClC,aAAa,EAAE,8BAA8B;IAC7C,SAAS,EAAE,kBAAkB;IAC7B,kBAAkB,EAAE,sBAAsB;IAC1C,mBAAmB,EAAE,wBAAwB;IAC7C,SAAS,EAAE,OAAO;IAClB,UAAU,EAAE,QAAQ;IACpB,gBAAgB,EAAE,cAAc;IAChC,YAAY,EAAE,UAAU;IACxB,SAAS,EAAE,OAAO;IAClB,SAAS,EAAE,OAAO;IAClB,SAAS,EAAE,OAAO;IAClB,cAAc,EAAE,YAAY;IAC5B,eAAe,EAAE,mCAAmC;IACpD,SAAS,EAAE,mBAAmB;IAC9B,OAAO,EAAE,oBAAoB;IAC7B,UAAU,EAAE,cAAc;IAC1B,MAAM,EAAE,cAAc;IACtB,MAAM,EAAE,eAAe;CACxB,CAAC;AAEF;;;GAGG;AACH,SAAS,aAAa,CAAC,KAAa;IAClC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEtC,OAAO,oBAAoB,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;AAC7D,CAAC;AAED;;;GAGG;AACH,SAAS,SAAS,CAAC,KAAa;IAC9B,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC;QAAE,OAAO,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACvD,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAChC,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5B,OAAO,GAAG,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC;AACvE,CAAC;AAED;;;GAGG;AACH,SAAS,YAAY,CAAC,OAAe,EAAE,MAAc;IACnD,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,IAAI,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtD,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI;YAAE,IAAI,EAAE,CAAC;IAClC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,gFAAgF;AAEhF;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,kBAAkB,CAChC,OAAe,EACf,QAAgB,EAChB,aAAa,GAA8B,YAAY,EACvD,gBAAgB,GAAW,GAAG;IAE9B,IAAI,KAAmB,CAAC;IACxB,IAAI,SAAS,GAAG,gBAAgB,CAAC;IAEjC,IAAI,KAAK,CAAC,OAAO,CAAC,aAAa,CAAC,EAAE,CAAC;QACjC,KAAK,GAAG,aAAa,CAAC;IACxB,CAAC;SAAM,IAAI,aAAa,IAAI,OAAO,aAAa,KAAK,QAAQ,EAAE,CAAC;QAC9D,oBAAoB;QACpB,MAAM,MAAM,GAAG,aAAa,CAAC;QAC7B,KAAK,GAAG,CAAC,GAAG,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,CAAC;QACzD,SAAS,GAAG,MAAM,CAAC,gBAAgB,IAAI,gBAAgB,CAAC;IAC1D,CAAC;SAAM,CAAC;QACN,KAAK,GAAG,YAAY,CAAC;IACvB,CAAC;IAED,MAAM,QAAQ,GAAkB,EAAE,CAAC;IAEnC,qDAAqD;IACrD,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAEtC,gEAAgE;IAChE,sCAAsC;IACtC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAE/B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,kDAAkD;QAClD,IAAI,CAAC,OAAO,CAAC,SAAS,GAAG,CAAC,CAAC;QAE3B,IAAI,KAA6B,CAAC;QAElC,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;YACrD,+DAA+D;YAC/D,MAAM,YAAY,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC;YAE1C,kCAAkC;YAClC,MAAM,SAAS,GAAG,GAAG,IAAI,CAAC,EAAE,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;YAC9C,IAAI,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC;gBAAE,SAAS;YAClC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YAEpB,oBAAoB;YACpB,IAAI,aAAa,CAAC,YAAY,CAAC;gBAAE,SAAS;YAE1C,0DAA0D;YAC1D,IAAI,IAAI,CAAC,EAAE,KAAK,sBAAsB,EAAE,CAAC;gBACvC,IAAI,CAAC,aAAa,CAAC,YAAY,EAAE,SAAS,CAAC;oBAAE,SAAS;YACxD,CAAC;YAED,6DAA6D;YAC7D,gDAAgD;YAChD,IAAI,IAAI,CAAC,EAAE,KAAK,gBAAgB,EAAE,CAAC;gBACjC,IAAI,CAAC,aAAa,CAAC,YAAY,EAAE,SAAS,CAAC;oBAAE,SAAS;YACxD,CAAC;YAED,MAAM,IAAI,GAAG,YAAY,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;YAEhD,sEAAsE;YACtE,MAAM,WAAW,GAAG,SAAS,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC;YACxC,IAAI,WAAW,EAAE,CAAC;gBAChB,MAAM,kBAAkB,GACtB,wEAAwE,CAAC,IAAI,CAC3E,WAAW,CAAC,IAAI,EAAE,CACnB,CAAC;gBACJ,IAAI,kBAAkB;oBAAE,SAAS;YACnC,CAAC;YAED,uEAAuE;YACvE,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YAChE,MAAM,SAAS,GACb,CAAC,WAAW;gBACV,CAAC,WAAW,CAAC,QAAQ,CAAC,gBAAgB,CAAC;oBACrC,WAAW,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC;gBACxC,CAAC,YAAY;oBACX,CAAC,YAAY,CAAC,QAAQ,CAAC,gBAAgB,CAAC;wBACtC,YAAY,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;YAE5C,IAAI,SAAS;gBAAE,SAAS;YACxB,MAAM,QAAQ,GAAG,cAAc,CAAC,YAAY,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,SAAS,CAAC;YAEpE,QAAQ,CAAC,IAAI,CAAC;gBACZ,EAAE,EAAE,MAAM,CAAC,QAAQ,CAAC;gBACpB,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,QAAQ,EAAE,iBAAiB;gBAC3B,OAAO,EACL,GAAG,IAAI,CAAC,IAAI,WAAW;oBACvB,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,QAAQ,CAAC,WAAW,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;gBAChD,IAAI,EAAE,QAAQ;gBACd,IAAI;gBACJ,UAAU,EAAE,QAAQ;oBAClB,CAAC,CAAC,sBAAsB,QAAQ,CAAC,WAAW,oCAAoC;oBAChF,CAAC,CAAC,+DAA+D;gBACnE,QAAQ;gBACR,MAAM,EAAE,IAAI,CAAC,EAAE;gBACf,OAAO,EAAE,SAAS,CAAC,YAAY,CAAC;aACjC,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,QAAgB,EAChB,KAAmB,EACnB,gBAAwB,EACxB,KAAK,GAAG,EAAE;IAEV,MAAM,QAAQ,GAAkB,EAAE,CAAC;IACnC,MAAM,GAAG,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;IAChC,GAAG,CAAC,GAAG,CAAC;QACN,GAAG,OAAO,CAAC,GAAG;QACd,OAAO,EAAE,SAAS;QAClB,aAAa,EAAE,SAAS;KACzB,CAAC,CAAC;IAEH,6BAA6B;IAC7B,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,WAAW,EAAE,CAAC;QACvC,IAAI,CAAC,MAAM;YAAE,OAAO,QAAQ,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,yBAAyB;IACzB,IAAI,GAAG,CAAC;IACR,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,GAAG,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;IAC3C,CAAC;IAAC,MAAM,CAAC;QACP,uCAAuC;QACvC,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,KAAK,MAAM,MAAM,IAAI,GAAG,CAAC,GAAG,EAAE,CAAC;QAC7B,IAAI,IAAY,CAAC;QACjB,IAAI,CAAC;YACH,oCAAoC;YACpC,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,MAAM,CAAC,IAAI,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;QAC3D,CAAC;QAAC,MAAM,CAAC;YACP,uDAAuD;YACvD,IAAI,CAAC;gBACH,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,CAAC;oBACpB,0CAA0C;oBAC1C,MAAM,CAAC,IAAI;iBACZ,CAAC,CAAC;YACL,CAAC;YAAC,MAAM,CAAC;gBACP,SAAS;YACX,CAAC;QACH,CAAC;QAED,qEAAqE;QACrE,oCAAoC;QACpC,MAAM,UAAU,GAAG,IAAI;aACpB,KAAK,CAAC,IAAI,CAAC;aACX,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;aACjE,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;aAC5B,IAAI,CAAC,IAAI,CAAC,CAAC;QAEd,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QAEtC,yBAAyB;QACzB,MAAM,cAAc,GAAG,kBAAkB,CACvC,UAAU,EACV,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAChC,KAAK,EACL,gBAAgB,CACjB,CAAC;QAEF,qCAAqC;QACrC,KAAK,MAAM,OAAO,IAAI,cAAc,EAAE,CAAC;YACrC,OAAO,CAAC,OAAO,IAAI,qBAAqB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC;YACnG,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACzB,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ScanFinding } from "../../types/index.js";
|
|
2
|
+
export interface CategoryBreakdown {
|
|
3
|
+
category: string;
|
|
4
|
+
points: number;
|
|
5
|
+
count: number;
|
|
6
|
+
}
|
|
7
|
+
export interface HealthReport {
|
|
8
|
+
/** Numeric score from 0 (worst) to 100 (perfect). */
|
|
9
|
+
score: number;
|
|
10
|
+
/** Letter grade (A+ through F). */
|
|
11
|
+
grade: string;
|
|
12
|
+
/** Per-category breakdown showing point deductions and finding counts. */
|
|
13
|
+
breakdown: CategoryBreakdown[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Calculate a health score from an array of `ScanFinding`s.
|
|
17
|
+
*
|
|
18
|
+
* Scoring:
|
|
19
|
+
* • Start at **100**.
|
|
20
|
+
* • Each `critical` finding: **−15 points**.
|
|
21
|
+
* • Each `warning` finding: **−5 points**.
|
|
22
|
+
* • Each `info` finding: **−1 point**.
|
|
23
|
+
* • Floor at **0** (score never goes negative).
|
|
24
|
+
*
|
|
25
|
+
* The `breakdown` array lists every finding category that contributed
|
|
26
|
+
* to the score loss, sorted by point deduction (largest first).
|
|
27
|
+
*/
|
|
28
|
+
export declare function calculateHealthScore(findings: ScanFinding[]): HealthReport;
|
|
29
|
+
//# sourceMappingURL=health.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../../../src/core/score/health.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,WAAW,EAGZ,MAAM,sBAAsB,CAAC;AA6B9B,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IAC3B,qDAAqD;IACrD,KAAK,EAAE,MAAM,CAAC;IACd,mCAAmC;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,SAAS,EAAE,iBAAiB,EAAE,CAAC;CAChC;AAID;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,YAAY,CAwC1E"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// ─── Health Score Calculator ─────────────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Computes a 0-100 health score from scan findings, with a letter-grade
|
|
4
|
+
// and per-category breakdown. Pure function — no side effects.
|
|
5
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
// ─── Point Deductions ────────────────────────────────────────────────────────
|
|
7
|
+
const SEVERITY_COST = {
|
|
8
|
+
critical: 15,
|
|
9
|
+
warning: 5,
|
|
10
|
+
info: 1,
|
|
11
|
+
};
|
|
12
|
+
// ─── Grade Thresholds ────────────────────────────────────────────────────────
|
|
13
|
+
// Ordered from highest to lowest so the first match wins.
|
|
14
|
+
const GRADE_THRESHOLDS = [
|
|
15
|
+
{ min: 97, grade: "A+" },
|
|
16
|
+
{ min: 93, grade: "A" },
|
|
17
|
+
{ min: 90, grade: "A-" },
|
|
18
|
+
{ min: 87, grade: "B+" },
|
|
19
|
+
{ min: 83, grade: "B" },
|
|
20
|
+
{ min: 80, grade: "B-" },
|
|
21
|
+
{ min: 77, grade: "C+" },
|
|
22
|
+
{ min: 73, grade: "C" },
|
|
23
|
+
{ min: 70, grade: "C-" },
|
|
24
|
+
{ min: 60, grade: "D" },
|
|
25
|
+
{ min: 0, grade: "F" },
|
|
26
|
+
];
|
|
27
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
28
|
+
/**
|
|
29
|
+
* Calculate a health score from an array of `ScanFinding`s.
|
|
30
|
+
*
|
|
31
|
+
* Scoring:
|
|
32
|
+
* • Start at **100**.
|
|
33
|
+
* • Each `critical` finding: **−15 points**.
|
|
34
|
+
* • Each `warning` finding: **−5 points**.
|
|
35
|
+
* • Each `info` finding: **−1 point**.
|
|
36
|
+
* • Floor at **0** (score never goes negative).
|
|
37
|
+
*
|
|
38
|
+
* The `breakdown` array lists every finding category that contributed
|
|
39
|
+
* to the score loss, sorted by point deduction (largest first).
|
|
40
|
+
*/
|
|
41
|
+
export function calculateHealthScore(findings) {
|
|
42
|
+
// Accumulate deductions per category
|
|
43
|
+
const categoryMap = new Map();
|
|
44
|
+
for (const finding of findings) {
|
|
45
|
+
const cost = SEVERITY_COST[finding.severity];
|
|
46
|
+
const existing = categoryMap.get(finding.category);
|
|
47
|
+
if (existing) {
|
|
48
|
+
existing.points += cost;
|
|
49
|
+
existing.count += 1;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
categoryMap.set(finding.category, { points: cost, count: 1 });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Total deduction
|
|
56
|
+
let totalDeduction = 0;
|
|
57
|
+
for (const { points } of categoryMap.values()) {
|
|
58
|
+
totalDeduction += points;
|
|
59
|
+
}
|
|
60
|
+
const score = Math.max(0, 100 - totalDeduction);
|
|
61
|
+
// Determine grade
|
|
62
|
+
const grade = GRADE_THRESHOLDS.find((t) => score >= t.min)?.grade ?? "F";
|
|
63
|
+
// Build sorted breakdown
|
|
64
|
+
const breakdown = [...categoryMap.entries()]
|
|
65
|
+
.map(([category, data]) => ({
|
|
66
|
+
category,
|
|
67
|
+
points: data.points,
|
|
68
|
+
count: data.count,
|
|
69
|
+
}))
|
|
70
|
+
.sort((a, b) => b.points - a.points);
|
|
71
|
+
return { score, grade, breakdown };
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=health.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health.js","sourceRoot":"","sources":["../../../src/core/score/health.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,EAAE;AACF,wEAAwE;AACxE,gEAAgE;AAChE,gFAAgF;AAQhF,gFAAgF;AAEhF,MAAM,aAAa,GAA6B;IAC9C,QAAQ,EAAE,EAAE;IACZ,OAAO,EAAE,CAAC;IACV,IAAI,EAAE,CAAC;CACR,CAAC;AAEF,gFAAgF;AAChF,0DAA0D;AAE1D,MAAM,gBAAgB,GAA0C;IAC9D,EAAE,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE;IACxB,EAAE,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE;IACvB,EAAE,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE;IACxB,EAAE,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE;IACxB,EAAE,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE;IACvB,EAAE,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE;IACxB,EAAE,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE;IACxB,EAAE,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE;IACvB,EAAE,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE;IACxB,EAAE,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE;IACvB,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE;CACvB,CAAC;AAmBF,gFAAgF;AAEhF;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,oBAAoB,CAAC,QAAuB;IAC1D,qCAAqC;IACrC,MAAM,WAAW,GAAG,IAAI,GAAG,EAGxB,CAAC;IAEJ,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,IAAI,GAAG,aAAa,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC7C,MAAM,QAAQ,GAAG,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAEnD,IAAI,QAAQ,EAAE,CAAC;YACb,QAAQ,CAAC,MAAM,IAAI,IAAI,CAAC;YACxB,QAAQ,CAAC,KAAK,IAAI,CAAC,CAAC;QACtB,CAAC;aAAM,CAAC;YACN,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;QAChE,CAAC;IACH,CAAC;IAED,kBAAkB;IAClB,IAAI,cAAc,GAAG,CAAC,CAAC;IACvB,KAAK,MAAM,EAAE,MAAM,EAAE,IAAI,WAAW,CAAC,MAAM,EAAE,EAAE,CAAC;QAC9C,cAAc,IAAI,MAAM,CAAC;IAC3B,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,GAAG,cAAc,CAAC,CAAC;IAEhD,kBAAkB;IAClB,MAAM,KAAK,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,KAAK,IAAI,GAAG,CAAC;IAEzE,yBAAyB;IACzB,MAAM,SAAS,GAAwB,CAAC,GAAG,WAAW,CAAC,OAAO,EAAE,CAAC;SAC9D,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;QAC1B,QAAQ;QACR,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,KAAK,EAAE,IAAI,CAAC,KAAK;KAClB,CAAC,CAAC;SACF,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC;IAEvC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;AACrC,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { BiltConfig, WatchEvent } from "../../types/index.js";
|
|
2
|
+
export interface WatcherHandle {
|
|
3
|
+
close: () => Promise<void>;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Start watching a project directory for file changes.
|
|
7
|
+
*
|
|
8
|
+
* On each change:
|
|
9
|
+
* 1. Read the modified file.
|
|
10
|
+
* 2. Run secret-detection rules against its content.
|
|
11
|
+
* 3. Invoke the `onEvent` callback with the resulting findings.
|
|
12
|
+
*
|
|
13
|
+
* The watcher ignores `node_modules`, `.git`, `.bilt`, `dist`, `build`,
|
|
14
|
+
* and binary files. Changes are debounced (default 300ms) to avoid
|
|
15
|
+
* flooding the callback during bulk operations like `npm install`.
|
|
16
|
+
*
|
|
17
|
+
* @param projectDir Root directory to watch.
|
|
18
|
+
* @param config Bilt configuration (used for entropy threshold & ignore patterns).
|
|
19
|
+
* @param onEvent Callback invoked with watch events.
|
|
20
|
+
* @returns A handle with a `close()` method to stop watching.
|
|
21
|
+
*/
|
|
22
|
+
export declare function startWatcher(projectDir: string, config: BiltConfig, onEvent: (event: WatchEvent) => void): WatcherHandle;
|
|
23
|
+
/**
|
|
24
|
+
* Stop a running watcher.
|
|
25
|
+
*
|
|
26
|
+
* @param watcher The watcher handle returned by `startWatcher`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function stopWatcher(watcher: WatcherHandle): Promise<void>;
|
|
29
|
+
//# sourceMappingURL=watcher.d.ts.map
|