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.
Files changed (116) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +168 -0
  3. package/bin/bilt.js +2 -0
  4. package/dist/cli.d.ts +3 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +161 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/commands/doctor.d.ts +13 -0
  9. package/dist/commands/doctor.d.ts.map +1 -0
  10. package/dist/commands/doctor.js +194 -0
  11. package/dist/commands/doctor.js.map +1 -0
  12. package/dist/commands/fix.d.ts +12 -0
  13. package/dist/commands/fix.d.ts.map +1 -0
  14. package/dist/commands/fix.js +273 -0
  15. package/dist/commands/fix.js.map +1 -0
  16. package/dist/commands/init.d.ts +11 -0
  17. package/dist/commands/init.d.ts.map +1 -0
  18. package/dist/commands/init.js +101 -0
  19. package/dist/commands/init.js.map +1 -0
  20. package/dist/commands/scan.d.ts +17 -0
  21. package/dist/commands/scan.d.ts.map +1 -0
  22. package/dist/commands/scan.js +267 -0
  23. package/dist/commands/scan.js.map +1 -0
  24. package/dist/commands/undo.d.ts +12 -0
  25. package/dist/commands/undo.d.ts.map +1 -0
  26. package/dist/commands/undo.js +90 -0
  27. package/dist/commands/undo.js.map +1 -0
  28. package/dist/commands/watch.d.ts +10 -0
  29. package/dist/commands/watch.d.ts.map +1 -0
  30. package/dist/commands/watch.js +68 -0
  31. package/dist/commands/watch.js.map +1 -0
  32. package/dist/config/config.d.ts +8 -0
  33. package/dist/config/config.d.ts.map +1 -0
  34. package/dist/config/config.js +75 -0
  35. package/dist/config/config.js.map +1 -0
  36. package/dist/core/fix/confirm.d.ts +21 -0
  37. package/dist/core/fix/confirm.d.ts.map +1 -0
  38. package/dist/core/fix/confirm.js +60 -0
  39. package/dist/core/fix/confirm.js.map +1 -0
  40. package/dist/core/fix/env-fix.d.ts +40 -0
  41. package/dist/core/fix/env-fix.d.ts.map +1 -0
  42. package/dist/core/fix/env-fix.js +186 -0
  43. package/dist/core/fix/env-fix.js.map +1 -0
  44. package/dist/core/fix/snapshot.d.ts +30 -0
  45. package/dist/core/fix/snapshot.d.ts.map +1 -0
  46. package/dist/core/fix/snapshot.js +186 -0
  47. package/dist/core/fix/snapshot.js.map +1 -0
  48. package/dist/core/rules/entropy.d.ts +29 -0
  49. package/dist/core/rules/entropy.d.ts.map +1 -0
  50. package/dist/core/rules/entropy.js +117 -0
  51. package/dist/core/rules/entropy.js.map +1 -0
  52. package/dist/core/rules/providers.d.ts +12 -0
  53. package/dist/core/rules/providers.d.ts.map +1 -0
  54. package/dist/core/rules/providers.js +184 -0
  55. package/dist/core/rules/providers.js.map +1 -0
  56. package/dist/core/rules/secret-rules.d.ts +3 -0
  57. package/dist/core/rules/secret-rules.d.ts.map +1 -0
  58. package/dist/core/rules/secret-rules.js +205 -0
  59. package/dist/core/rules/secret-rules.js.map +1 -0
  60. package/dist/core/scan/env.d.ts +47 -0
  61. package/dist/core/scan/env.d.ts.map +1 -0
  62. package/dist/core/scan/env.js +301 -0
  63. package/dist/core/scan/env.js.map +1 -0
  64. package/dist/core/scan/framework.d.ts +26 -0
  65. package/dist/core/scan/framework.d.ts.map +1 -0
  66. package/dist/core/scan/framework.js +254 -0
  67. package/dist/core/scan/framework.js.map +1 -0
  68. package/dist/core/scan/gitignore.d.ts +20 -0
  69. package/dist/core/scan/gitignore.d.ts.map +1 -0
  70. package/dist/core/scan/gitignore.js +217 -0
  71. package/dist/core/scan/gitignore.js.map +1 -0
  72. package/dist/core/scan/secrets.d.ts +27 -0
  73. package/dist/core/scan/secrets.d.ts.map +1 -0
  74. package/dist/core/scan/secrets.js +247 -0
  75. package/dist/core/scan/secrets.js.map +1 -0
  76. package/dist/core/score/health.d.ts +29 -0
  77. package/dist/core/score/health.d.ts.map +1 -0
  78. package/dist/core/score/health.js +73 -0
  79. package/dist/core/score/health.js.map +1 -0
  80. package/dist/core/watch/watcher.d.ts +29 -0
  81. package/dist/core/watch/watcher.d.ts.map +1 -0
  82. package/dist/core/watch/watcher.js +176 -0
  83. package/dist/core/watch/watcher.js.map +1 -0
  84. package/dist/index.d.ts +2 -0
  85. package/dist/index.d.ts.map +1 -0
  86. package/dist/index.js +2 -0
  87. package/dist/index.js.map +1 -0
  88. package/dist/plugins/interface.d.ts +12 -0
  89. package/dist/plugins/interface.d.ts.map +1 -0
  90. package/dist/plugins/interface.js +91 -0
  91. package/dist/plugins/interface.js.map +1 -0
  92. package/dist/plugins/loader.d.ts +10 -0
  93. package/dist/plugins/loader.d.ts.map +1 -0
  94. package/dist/plugins/loader.js +104 -0
  95. package/dist/plugins/loader.js.map +1 -0
  96. package/dist/plugins/official/docker.d.ts +4 -0
  97. package/dist/plugins/official/docker.d.ts.map +1 -0
  98. package/dist/plugins/official/docker.js +111 -0
  99. package/dist/plugins/official/docker.js.map +1 -0
  100. package/dist/plugins/official/terraform.d.ts +4 -0
  101. package/dist/plugins/official/terraform.d.ts.map +1 -0
  102. package/dist/plugins/official/terraform.js +131 -0
  103. package/dist/plugins/official/terraform.js.map +1 -0
  104. package/dist/types/index.d.ts +158 -0
  105. package/dist/types/index.d.ts.map +1 -0
  106. package/dist/types/index.js +3 -0
  107. package/dist/types/index.js.map +1 -0
  108. package/dist/ui/format.d.ts +43 -0
  109. package/dist/ui/format.d.ts.map +1 -0
  110. package/dist/ui/format.js +207 -0
  111. package/dist/ui/format.js.map +1 -0
  112. package/dist/ui/reporter.d.ts +39 -0
  113. package/dist/ui/reporter.d.ts.map +1 -0
  114. package/dist/ui/reporter.js +254 -0
  115. package/dist/ui/reporter.js.map +1 -0
  116. 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":"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"}
@@ -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