@panguard-ai/panguard-scan 0.2.0 → 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.
@@ -0,0 +1,289 @@
1
+ /**
2
+ * SAST (Static Application Security Testing) - Semgrep integration
3
+ * 靜態應用程式安全測試 - Semgrep 整合
4
+ *
5
+ * Runs Semgrep-based SAST analysis when semgrep is installed.
6
+ * If Semgrep is not available, returns an empty result with an info message.
7
+ * 當 semgrep 已安裝時執行基於 Semgrep 的 SAST 分析。
8
+ * 若 Semgrep 不可用,回傳空結果並附帶提示訊息。
9
+ *
10
+ * @module @panguard-ai/panguard-scan/scanners/sast-checker
11
+ */
12
+ import { execFile } from 'node:child_process';
13
+ import { promisify } from 'node:util';
14
+ import { promises as fs } from 'node:fs';
15
+ import path from 'node:path';
16
+ import { createLogger } from '@panguard-ai/core';
17
+ const logger = createLogger('panguard-scan:sast');
18
+ const execFileAsync = promisify(execFile);
19
+ /**
20
+ * Semgrep execution timeout in milliseconds (60 seconds)
21
+ * Semgrep 執行逾時(毫秒,60 秒)
22
+ */
23
+ const SEMGREP_TIMEOUT_MS = 60_000;
24
+ /**
25
+ * Check if semgrep is available on the PATH
26
+ * 檢查 semgrep 是否在 PATH 上可用
27
+ *
28
+ * @returns True if semgrep is available / 若 semgrep 可用則為 true
29
+ */
30
+ async function isSemgrepAvailable() {
31
+ try {
32
+ await execFileAsync('which', ['semgrep'], { timeout: 5_000 });
33
+ return true;
34
+ }
35
+ catch {
36
+ try {
37
+ await execFileAsync('semgrep', ['--version'], { timeout: 5_000 });
38
+ return true;
39
+ }
40
+ catch {
41
+ return false;
42
+ }
43
+ }
44
+ }
45
+ /**
46
+ * Map semgrep severity string to Severity type
47
+ * 將 semgrep 嚴重度字串映射到 Severity 型別
48
+ *
49
+ * @param semgrepSeverity - Semgrep severity string / Semgrep 嚴重度字串
50
+ * @param metadataSeverity - Optional metadata severity override / 可選的元資料嚴重度覆蓋
51
+ * @returns Mapped severity level / 映射後的嚴重等級
52
+ */
53
+ function mapSemgrepSeverity(semgrepSeverity, metadataSeverity) {
54
+ if (metadataSeverity) {
55
+ const upper = metadataSeverity.toUpperCase();
56
+ if (upper === 'CRITICAL')
57
+ return 'critical';
58
+ if (upper === 'HIGH')
59
+ return 'high';
60
+ if (upper === 'MEDIUM')
61
+ return 'medium';
62
+ if (upper === 'LOW')
63
+ return 'low';
64
+ }
65
+ switch (semgrepSeverity.toUpperCase()) {
66
+ case 'ERROR':
67
+ return 'high';
68
+ case 'WARNING':
69
+ return 'medium';
70
+ default:
71
+ return 'low';
72
+ }
73
+ }
74
+ /**
75
+ * Derive a remediation message from a semgrep check_id
76
+ * 從 semgrep check_id 推導修復建議訊息
77
+ *
78
+ * @param checkId - Semgrep rule ID / Semgrep 規則 ID
79
+ * @returns Remediation recommendation / 修復建議
80
+ */
81
+ function deriveRemediation(checkId) {
82
+ const lower = checkId.toLowerCase();
83
+ if (lower.includes('secret') || lower.includes('password') || lower.includes('key')) {
84
+ return ('Move secrets to environment variables or a dedicated secrets manager. / ' +
85
+ '將密鑰移至環境變數或專用密鑰管理員。');
86
+ }
87
+ if (lower.includes('sql') || lower.includes('injection') || lower.includes('sqli')) {
88
+ return ('Use parameterized queries or prepared statements to prevent SQL injection. / ' +
89
+ '使用參數化查詢或預備語句以防止 SQL 注入。');
90
+ }
91
+ if (lower.includes('xss') || lower.includes('html') || lower.includes('innerhtml')) {
92
+ return ('Sanitize user-supplied content before rendering it as HTML. / ' +
93
+ '在將使用者提供的內容呈現為 HTML 之前進行清理。');
94
+ }
95
+ if (lower.includes('eval') || lower.includes('exec') || lower.includes('command')) {
96
+ return ('Avoid executing user-controlled input as code or system commands. / ' +
97
+ '避免將使用者控制的輸入作為代碼或系統命令執行。');
98
+ }
99
+ if (lower.includes('crypto') || lower.includes('hash') || lower.includes('random')) {
100
+ return ('Use cryptographically secure algorithms and random number generators. / ' +
101
+ '使用加密安全的演算法和隨機數生成器。');
102
+ }
103
+ if (lower.includes('tls') || lower.includes('ssl') || lower.includes('cert')) {
104
+ return ('Ensure proper TLS/SSL configuration and certificate validation. / ' +
105
+ '確保正確的 TLS/SSL 配置和憑證驗證。');
106
+ }
107
+ return ('Review and remediate this security finding according to your security policy. / ' +
108
+ '根據您的安全策略審查並修復此安全發現。');
109
+ }
110
+ /**
111
+ * Map OWASP reference string to a compliance ref
112
+ * 將 OWASP 參照字串映射到合規參照
113
+ *
114
+ * @param owasp - OWASP category / OWASP 類別
115
+ * @returns Compliance reference string or undefined / 合規參照字串或 undefined
116
+ */
117
+ function mapOwaspToComplianceRef(owasp) {
118
+ if (!owasp)
119
+ return undefined;
120
+ const owaspStr = Array.isArray(owasp) ? owasp.join(' ') : owasp;
121
+ const lower = owaspStr.toLowerCase();
122
+ if (lower.includes('a01') || lower.includes('broken access'))
123
+ return '4.1';
124
+ if (lower.includes('a02') || lower.includes('cryptographic'))
125
+ return '4.4';
126
+ if (lower.includes('a03') || lower.includes('injection'))
127
+ return '4.3';
128
+ if (lower.includes('a07') || lower.includes('identification'))
129
+ return '4.5';
130
+ if (lower.includes('a09') || lower.includes('logging'))
131
+ return '4.7';
132
+ return undefined;
133
+ }
134
+ /**
135
+ * Convert a semgrep result to a Finding
136
+ * 將 semgrep 結果轉換為 Finding
137
+ *
138
+ * @param result - Semgrep result item / Semgrep 結果項目
139
+ * @returns Converted Finding / 轉換後的 Finding
140
+ */
141
+ function semgrepResultToFinding(result) {
142
+ const checkId = result.check_id;
143
+ const lineNum = result.start.line;
144
+ const col = result.start.col;
145
+ const filePath = result.path;
146
+ // Build a safe ID from the check_id last segment + line number
147
+ // 從 check_id 最後片段和行號建立安全 ID
148
+ const idSuffix = (checkId.split('.').pop() ?? checkId)
149
+ .toUpperCase()
150
+ .replace(/[^A-Z0-9-_]/g, '-')
151
+ .slice(0, 30);
152
+ const id = `SAST-${idSuffix}-${lineNum}`;
153
+ const message = result.extra.message;
154
+ const title = message.length > 80 ? message.slice(0, 80) : message;
155
+ const description = `${message}\n\nFile: ${filePath}, Line: ${lineNum}, Col: ${col}`;
156
+ const severity = mapSemgrepSeverity(result.extra.severity, result.extra.metadata?.severity);
157
+ const remediation = deriveRemediation(checkId);
158
+ const cwe = result.extra.metadata?.cwe;
159
+ const owasp = result.extra.metadata?.owasp;
160
+ const complianceRef = mapOwaspToComplianceRef(owasp) ?? (cwe ? '4.3' : undefined);
161
+ return {
162
+ id,
163
+ title,
164
+ description,
165
+ severity,
166
+ category: 'code',
167
+ remediation,
168
+ complianceRef,
169
+ details: `${filePath}:${lineNum}:${col}`,
170
+ };
171
+ }
172
+ /**
173
+ * Run semgrep and parse the JSON output into findings
174
+ * 執行 semgrep 並將 JSON 輸出解析為發現
175
+ *
176
+ * @param targetDir - Directory to scan / 要掃描的目錄
177
+ * @returns Array of findings from semgrep / 來自 semgrep 的發現陣列
178
+ */
179
+ async function runSemgrep(targetDir) {
180
+ logger.info('Running semgrep SAST scan', { targetDir });
181
+ try {
182
+ const { stdout } = await execFileAsync('semgrep', [
183
+ '--json',
184
+ '--config=p/security-audit',
185
+ '--config=p/secrets',
186
+ '--no-git-ignore',
187
+ '--timeout=60',
188
+ targetDir,
189
+ ], { timeout: SEMGREP_TIMEOUT_MS });
190
+ let parsed;
191
+ try {
192
+ parsed = JSON.parse(stdout);
193
+ }
194
+ catch (parseErr) {
195
+ logger.warn('Failed to parse semgrep JSON output', {
196
+ error: parseErr instanceof Error ? parseErr.message : String(parseErr),
197
+ });
198
+ return [];
199
+ }
200
+ const results = parsed.results ?? [];
201
+ logger.info(`Semgrep found ${results.length} raw result(s)`);
202
+ // Convert results to findings, deduplicating by check_id + path + line
203
+ // 將結果轉換為發現,按 check_id + 路徑 + 行號去重
204
+ const seen = new Set();
205
+ const findings = [];
206
+ for (const result of results) {
207
+ const dedupeKey = `${result.check_id}:${result.path}:${result.start.line}`;
208
+ if (seen.has(dedupeKey))
209
+ continue;
210
+ seen.add(dedupeKey);
211
+ findings.push(semgrepResultToFinding(result));
212
+ }
213
+ logger.info(`Semgrep produced ${findings.length} deduplicated finding(s)`);
214
+ return findings;
215
+ }
216
+ catch (err) {
217
+ // Exit code 1 from semgrep means findings were found (not an error)
218
+ // semgrep 退出碼 1 表示有發現(非錯誤)
219
+ if (err &&
220
+ typeof err === 'object' &&
221
+ 'code' in err &&
222
+ err.code === 1 &&
223
+ 'stdout' in err &&
224
+ typeof err.stdout === 'string') {
225
+ const stdout = err.stdout;
226
+ try {
227
+ const parsed = JSON.parse(stdout);
228
+ const results = parsed.results ?? [];
229
+ logger.info(`Semgrep (exit 1) found ${results.length} result(s)`);
230
+ const seen = new Set();
231
+ const findings = [];
232
+ for (const result of results) {
233
+ const dedupeKey = `${result.check_id}:${result.path}:${result.start.line}`;
234
+ if (seen.has(dedupeKey))
235
+ continue;
236
+ seen.add(dedupeKey);
237
+ findings.push(semgrepResultToFinding(result));
238
+ }
239
+ return findings;
240
+ }
241
+ catch {
242
+ logger.warn('Failed to parse semgrep exit-1 output');
243
+ return [];
244
+ }
245
+ }
246
+ logger.warn('Semgrep execution failed', {
247
+ error: err instanceof Error ? err.message : String(err),
248
+ });
249
+ return [];
250
+ }
251
+ }
252
+ /**
253
+ * Scan source code for security vulnerabilities using SAST
254
+ * 使用 SAST 掃描原始碼的安全漏洞
255
+ *
256
+ * Runs Semgrep if available. If Semgrep is not installed, returns an empty
257
+ * result array. Install Semgrep for full SAST coverage.
258
+ * 若 Semgrep 可用則執行。若 Semgrep 未安裝,回傳空結果陣列。
259
+ * 安裝 Semgrep 以取得完整 SAST 覆蓋。
260
+ *
261
+ * @param targetDir - Source code directory to scan / 要掃描的原始碼目錄
262
+ * @returns Array of security findings / 安全發現陣列
263
+ */
264
+ export async function checkSourceCode(targetDir) {
265
+ // Validate targetDir exists
266
+ // 驗證 targetDir 存在
267
+ try {
268
+ const stat = await fs.stat(targetDir);
269
+ if (!stat.isDirectory()) {
270
+ logger.warn(`Target path is not a directory: ${targetDir}`);
271
+ return [];
272
+ }
273
+ }
274
+ catch (err) {
275
+ logger.warn(`Target directory does not exist or is not accessible: ${targetDir}`, {
276
+ error: err instanceof Error ? err.message : String(err),
277
+ });
278
+ return [];
279
+ }
280
+ const resolvedDir = path.resolve(targetDir);
281
+ const semgrepAvailable = await isSemgrepAvailable();
282
+ if (semgrepAvailable) {
283
+ logger.info('Semgrep is available, running SAST scan', { targetDir: resolvedDir });
284
+ return runSemgrep(resolvedDir);
285
+ }
286
+ logger.info('Semgrep is not installed. Install Semgrep for SAST scanning: https://semgrep.dev');
287
+ return [];
288
+ }
289
+ //# sourceMappingURL=sast-checker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sast-checker.js","sourceRoot":"","sources":["../../src/scanners/sast-checker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAIjD,MAAM,MAAM,GAAG,YAAY,CAAC,oBAAoB,CAAC,CAAC;AAClD,MAAM,aAAa,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAE1C;;;GAGG;AACH,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAgClC;;;;;GAKG;AACH,KAAK,UAAU,kBAAkB;IAC/B,IAAI,CAAC;QACH,MAAM,aAAa,CAAC,OAAO,EAAE,CAAC,SAAS,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QAC9D,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,IAAI,CAAC;YACH,MAAM,aAAa,CAAC,SAAS,EAAE,CAAC,WAAW,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;YAClE,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,kBAAkB,CAAC,eAAuB,EAAE,gBAAyB;IAC5E,IAAI,gBAAgB,EAAE,CAAC;QACrB,MAAM,KAAK,GAAG,gBAAgB,CAAC,WAAW,EAAE,CAAC;QAC7C,IAAI,KAAK,KAAK,UAAU;YAAE,OAAO,UAAU,CAAC;QAC5C,IAAI,KAAK,KAAK,MAAM;YAAE,OAAO,MAAM,CAAC;QACpC,IAAI,KAAK,KAAK,QAAQ;YAAE,OAAO,QAAQ,CAAC;QACxC,IAAI,KAAK,KAAK,KAAK;YAAE,OAAO,KAAK,CAAC;IACpC,CAAC;IAED,QAAQ,eAAe,CAAC,WAAW,EAAE,EAAE,CAAC;QACtC,KAAK,OAAO;YACV,OAAO,MAAM,CAAC;QAChB,KAAK,SAAS;YACZ,OAAO,QAAQ,CAAC;QAClB;YACE,OAAO,KAAK,CAAC;IACjB,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAS,iBAAiB,CAAC,OAAe;IACxC,MAAM,KAAK,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAEpC,IAAI,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QACpF,OAAO,CACL,0EAA0E;YAC1E,oBAAoB,CACrB,CAAC;IACJ,CAAC;IACD,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QACnF,OAAO,CACL,+EAA+E;YAC/E,yBAAyB,CAC1B,CAAC;IACJ,CAAC;IACD,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;QACnF,OAAO,CACL,gEAAgE;YAChE,4BAA4B,CAC7B,CAAC;IACJ,CAAC;IACD,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;QAClF,OAAO,CACL,sEAAsE;YACtE,yBAAyB,CAC1B,CAAC;IACJ,CAAC;IACD,IAAI,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QACnF,OAAO,CACL,0EAA0E;YAC1E,oBAAoB,CACrB,CAAC;IACJ,CAAC;IACD,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC7E,OAAO,CACL,oEAAoE;YACpE,wBAAwB,CACzB,CAAC;IACJ,CAAC;IAED,OAAO,CACL,kFAAkF;QAClF,qBAAqB,CACtB,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,SAAS,uBAAuB,CAAC,KAAyB;IACxD,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAE7B,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IAChE,MAAM,KAAK,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC;IAErC,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,eAAe,CAAC;QAAE,OAAO,KAAK,CAAC;IAC3E,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,eAAe,CAAC;QAAE,OAAO,KAAK,CAAC;IAC3E,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC;QAAE,OAAO,KAAK,CAAC;IACvE,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,gBAAgB,CAAC;QAAE,OAAO,KAAK,CAAC;IAC5E,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC;QAAE,OAAO,KAAK,CAAC;IAErE,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;GAMG;AACH,SAAS,sBAAsB,CAAC,MAAqB;IACnD,MAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC;IAChC,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC;IAClC,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC;IAC7B,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC;IAE7B,+DAA+D;IAC/D,4BAA4B;IAC5B,MAAM,QAAQ,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,OAAO,CAAC;SACnD,WAAW,EAAE;SACb,OAAO,CAAC,cAAc,EAAE,GAAG,CAAC;SAC5B,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAChB,MAAM,EAAE,GAAG,QAAQ,QAAQ,IAAI,OAAO,EAAE,CAAC;IAEzC,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC;IACrC,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;IACnE,MAAM,WAAW,GAAG,GAAG,OAAO,aAAa,QAAQ,WAAW,OAAO,UAAU,GAAG,EAAE,CAAC;IAErF,MAAM,QAAQ,GAAG,kBAAkB,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAE5F,MAAM,WAAW,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAC;IAE/C,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,GAAG,CAAC;IACvC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC;IAC3C,MAAM,aAAa,GAAG,uBAAuB,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAElF,OAAO;QACL,EAAE;QACF,KAAK;QACL,WAAW;QACX,QAAQ;QACR,QAAQ,EAAE,MAAM;QAChB,WAAW;QACX,aAAa;QACb,OAAO,EAAE,GAAG,QAAQ,IAAI,OAAO,IAAI,GAAG,EAAE;KACzC,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,KAAK,UAAU,UAAU,CAAC,SAAiB;IACzC,MAAM,CAAC,IAAI,CAAC,2BAA2B,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC;IAExD,IAAI,CAAC;QACH,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,aAAa,CACpC,SAAS,EACT;YACE,QAAQ;YACR,2BAA2B;YAC3B,oBAAoB;YACpB,iBAAiB;YACjB,cAAc;YACd,SAAS;SACV,EACD,EAAE,OAAO,EAAE,kBAAkB,EAAE,CAChC,CAAC;QAEF,IAAI,MAAqB,CAAC;QAC1B,IAAI,CAAC;YACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAkB,CAAC;QAC/C,CAAC;QAAC,OAAO,QAAQ,EAAE,CAAC;YAClB,MAAM,CAAC,IAAI,CAAC,qCAAqC,EAAE;gBACjD,KAAK,EAAE,QAAQ,YAAY,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC;aACvE,CAAC,CAAC;YACH,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC;QACrC,MAAM,CAAC,IAAI,CAAC,iBAAiB,OAAO,CAAC,MAAM,gBAAgB,CAAC,CAAC;QAE7D,uEAAuE;QACvE,kCAAkC;QAClC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;QAC/B,MAAM,QAAQ,GAAc,EAAE,CAAC;QAE/B,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC7B,MAAM,SAAS,GAAG,GAAG,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;YAC3E,IAAI,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC;gBAAE,SAAS;YAClC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YACpB,QAAQ,CAAC,IAAI,CAAC,sBAAsB,CAAC,MAAM,CAAC,CAAC,CAAC;QAChD,CAAC;QAED,MAAM,CAAC,IAAI,CAAC,oBAAoB,QAAQ,CAAC,MAAM,0BAA0B,CAAC,CAAC;QAC3E,OAAO,QAAQ,CAAC;IAClB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,oEAAoE;QACpE,2BAA2B;QAC3B,IACE,GAAG;YACH,OAAO,GAAG,KAAK,QAAQ;YACvB,MAAM,IAAI,GAAG;YACb,GAAG,CAAC,IAAI,KAAK,CAAC;YACd,QAAQ,IAAI,GAAG;YACf,OAAQ,GAA2B,CAAC,MAAM,KAAK,QAAQ,EACvD,CAAC;YACD,MAAM,MAAM,GAAI,GAA0B,CAAC,MAAM,CAAC;YAClD,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAkB,CAAC;gBACnD,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC;gBACrC,MAAM,CAAC,IAAI,CAAC,0BAA0B,OAAO,CAAC,MAAM,YAAY,CAAC,CAAC;gBAElE,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;gBAC/B,MAAM,QAAQ,GAAc,EAAE,CAAC;gBAE/B,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;oBAC7B,MAAM,SAAS,GAAG,GAAG,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;oBAC3E,IAAI,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC;wBAAE,SAAS;oBAClC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;oBACpB,QAAQ,CAAC,IAAI,CAAC,sBAAsB,CAAC,MAAM,CAAC,CAAC,CAAC;gBAChD,CAAC;gBAED,OAAO,QAAQ,CAAC;YAClB,CAAC;YAAC,MAAM,CAAC;gBACP,MAAM,CAAC,IAAI,CAAC,uCAAuC,CAAC,CAAC;gBACrD,OAAO,EAAE,CAAC;YACZ,CAAC;QACH,CAAC;QAED,MAAM,CAAC,IAAI,CAAC,0BAA0B,EAAE;YACtC,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;SACxD,CAAC,CAAC;QACH,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,SAAiB;IACrD,4BAA4B;IAC5B,kBAAkB;IAClB,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACtC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;YACxB,MAAM,CAAC,IAAI,CAAC,mCAAmC,SAAS,EAAE,CAAC,CAAC;YAC5D,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,IAAI,CAAC,yDAAyD,SAAS,EAAE,EAAE;YAChF,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;SACxD,CAAC,CAAC;QACH,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAE5C,MAAM,gBAAgB,GAAG,MAAM,kBAAkB,EAAE,CAAC;IAEpD,IAAI,gBAAgB,EAAE,CAAC;QACrB,MAAM,CAAC,IAAI,CAAC,yCAAyC,EAAE,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC,CAAC;QACnF,OAAO,UAAU,CAAC,WAAW,CAAC,CAAC;IACjC,CAAC;IAED,MAAM,CAAC,IAAI,CAAC,kFAAkF,CAAC,CAAC;IAChG,OAAO,EAAE,CAAC;AACZ,CAAC"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Hardcoded secrets scanner
3
+ * 硬編碼密鑰掃描器
4
+ *
5
+ * Scans source files for hardcoded secrets such as API keys, tokens, credentials,
6
+ * and private keys. Works independently of semgrep.
7
+ * 掃描原始碼檔案中的硬編碼密鑰,如 API 金鑰、令牌、憑證和私鑰。
8
+ * 獨立於 semgrep 運作。
9
+ *
10
+ * @module @panguard-ai/panguard-scan/scanners/secrets-checker
11
+ */
12
+ import type { Finding } from './types.js';
13
+ /**
14
+ * Scan source code directory for hardcoded secrets
15
+ * 掃描原始碼目錄中的硬編碼密鑰
16
+ *
17
+ * Detects common secret patterns including:
18
+ * - AWS Access Key IDs
19
+ * - GitHub tokens (ghp_, gho_, ghs_, ghr_ prefixes)
20
+ * - Slack tokens (xox[pboa]-)
21
+ * - Stripe live keys (sk_live_, pk_live_)
22
+ * - Bearer tokens in hardcoded strings
23
+ * - Private key headers (RSA, EC, etc.)
24
+ * - Database connection strings with credentials
25
+ * - Generic API keys in environment files
26
+ *
27
+ * 偵測常見的密鑰模式包括:
28
+ * - AWS 存取金鑰 ID
29
+ * - GitHub 令牌(ghp_、gho_、ghs_、ghr_ 前綴)
30
+ * - Slack 令牌(xox[pboa]-)
31
+ * - Stripe 正式金鑰(sk_live_、pk_live_)
32
+ * - 硬編碼字串中的 Bearer 令牌
33
+ * - 私鑰標頭(RSA、EC 等)
34
+ * - 含憑證的資料庫連線字串
35
+ * - 環境檔案中的通用 API 金鑰
36
+ *
37
+ * @param targetDir - Source code directory to scan / 要掃描的原始碼目錄
38
+ * @returns Array of secret findings / 密鑰發現陣列
39
+ */
40
+ export declare function checkHardcodedSecrets(targetDir: string): Promise<Finding[]>;
41
+ //# sourceMappingURL=secrets-checker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secrets-checker.d.ts","sourceRoot":"","sources":["../../src/scanners/secrets-checker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAmS1C;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAsB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAyCjF"}
@@ -0,0 +1,332 @@
1
+ /**
2
+ * Hardcoded secrets scanner
3
+ * 硬編碼密鑰掃描器
4
+ *
5
+ * Scans source files for hardcoded secrets such as API keys, tokens, credentials,
6
+ * and private keys. Works independently of semgrep.
7
+ * 掃描原始碼檔案中的硬編碼密鑰,如 API 金鑰、令牌、憑證和私鑰。
8
+ * 獨立於 semgrep 運作。
9
+ *
10
+ * @module @panguard-ai/panguard-scan/scanners/secrets-checker
11
+ */
12
+ import { promises as fs } from 'node:fs';
13
+ import path from 'node:path';
14
+ import { createLogger } from '@panguard-ai/core';
15
+ const logger = createLogger('panguard-scan:secrets-checker');
16
+ /**
17
+ * Maximum file size in bytes to scan (500 KB)
18
+ * 最大掃描檔案大小(500 KB)
19
+ */
20
+ const MAX_FILE_SIZE_BYTES = 500 * 1024;
21
+ /**
22
+ * Directories to skip when walking the source tree
23
+ * 遍歷原始碼樹時要跳過的目錄
24
+ */
25
+ const SKIP_DIRS = new Set([
26
+ 'node_modules',
27
+ '.git',
28
+ 'dist',
29
+ 'build',
30
+ 'coverage',
31
+ 'vendor',
32
+ '.next',
33
+ '__pycache__',
34
+ ]);
35
+ /**
36
+ * Source file extensions to scan for secrets
37
+ * 要掃描密鑰的原始碼檔案副檔名
38
+ */
39
+ const SCANNABLE_EXTENSIONS = new Set([
40
+ '.ts',
41
+ '.tsx',
42
+ '.js',
43
+ '.jsx',
44
+ '.mjs',
45
+ '.cjs',
46
+ '.py',
47
+ '.go',
48
+ '.java',
49
+ '.php',
50
+ '.rb',
51
+ '.cs',
52
+ '.rs',
53
+ '.cpp',
54
+ '.c',
55
+ '.h',
56
+ '.env',
57
+ '.env.local',
58
+ '.env.production',
59
+ '.env.staging',
60
+ '.env.development',
61
+ '.env.test',
62
+ '.yaml',
63
+ '.yml',
64
+ '.json',
65
+ '.toml',
66
+ '.ini',
67
+ '.cfg',
68
+ '.conf',
69
+ '.properties',
70
+ '.xml',
71
+ '.pem',
72
+ '.key',
73
+ '.crt',
74
+ '.cert',
75
+ ]);
76
+ /**
77
+ * Secret detection patterns
78
+ * 密鑰偵測模式
79
+ */
80
+ const SECRET_PATTERNS = [
81
+ {
82
+ id: 'SECRETS-AWS-KEY',
83
+ pattern: /\bAKIA[0-9A-Z]{16}\b/g,
84
+ title: 'AWS Access Key ID detected / 偵測到 AWS 存取金鑰 ID',
85
+ description: 'An AWS Access Key ID (starting with AKIA) was found hardcoded in source code. ' +
86
+ 'This credential can be used to access AWS services. / ' +
87
+ '在原始碼中發現硬編碼的 AWS 存取金鑰 ID(以 AKIA 開頭)。此憑證可用於存取 AWS 服務。',
88
+ },
89
+ {
90
+ id: 'SECRETS-GITHUB-TOKEN',
91
+ pattern: /\b(ghp|gho|ghs|ghr)_[A-Za-z0-9]{36,}\b/g,
92
+ title: 'GitHub token detected / 偵測到 GitHub 令牌',
93
+ description: 'A GitHub personal access token or OAuth token was found hardcoded in source code. ' +
94
+ 'This token can be used to access GitHub repositories and APIs. / ' +
95
+ '在原始碼中發現硬編碼的 GitHub 個人存取令牌或 OAuth 令牌。此令牌可用於存取 GitHub 儲存庫和 API。',
96
+ },
97
+ {
98
+ id: 'SECRETS-SLACK-TOKEN',
99
+ pattern: /\bxox[pboa]-[0-9A-Za-z-]{10,}\b/g,
100
+ title: 'Slack token detected / 偵測到 Slack 令牌',
101
+ description: 'A Slack API token was found hardcoded in source code. ' +
102
+ 'This token can be used to access Slack workspaces and data. / ' +
103
+ '在原始碼中發現硬編碼的 Slack API 令牌。此令牌可用於存取 Slack 工作區和資料。',
104
+ },
105
+ {
106
+ id: 'SECRETS-STRIPE-LIVE-KEY',
107
+ pattern: /\b(sk_live_|pk_live_)[0-9A-Za-z]{24,}\b/g,
108
+ title: 'Stripe live key detected / 偵測到 Stripe 正式金鑰',
109
+ description: 'A Stripe live API key was found hardcoded in source code. ' +
110
+ 'This key can be used to process real financial transactions. / ' +
111
+ '在原始碼中發現硬編碼的 Stripe 正式 API 金鑰。此金鑰可用於處理真實的金融交易。',
112
+ },
113
+ {
114
+ id: 'SECRETS-BEARER-TOKEN',
115
+ pattern: /["']Bearer\s+[A-Za-z0-9\-._~+/]{20,}={0,2}["']/g,
116
+ title: 'Hardcoded Bearer token detected / 偵測到硬編碼 Bearer 令牌',
117
+ description: 'A hardcoded Bearer token was found in source code. ' +
118
+ 'Bearer tokens grant access to protected APIs and should not be stored in code. / ' +
119
+ '在原始碼中發現硬編碼的 Bearer 令牌。Bearer 令牌授予對受保護 API 的存取權限,不應儲存在代碼中。',
120
+ },
121
+ {
122
+ id: 'SECRETS-RSA-PRIVATE-KEY',
123
+ pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g,
124
+ title: 'Private key material detected / 偵測到私鑰材料',
125
+ description: 'A private key header was found in source code or a committed file. ' +
126
+ 'Private keys should never be stored in source code repositories. / ' +
127
+ '在原始碼或已提交的檔案中發現私鑰標頭。私鑰絕不應儲存在原始碼儲存庫中。',
128
+ },
129
+ {
130
+ id: 'SECRETS-DB-CONNECTION-STRING',
131
+ pattern: /(?:mongodb(?:\+srv)?|postgresql|postgres|mysql|redis|amqp|rabbitmq):\/\/[^:/@\s]+:[^:/@\s]+@[^\s"'`]+/gi,
132
+ title: 'Database connection string with credentials / 含憑證的資料庫連線字串',
133
+ description: 'A database connection string with embedded credentials was found in source code. ' +
134
+ 'Database credentials should be managed via environment variables or secrets management. / ' +
135
+ '在原始碼中發現含嵌入憑證的資料庫連線字串。資料庫憑證應透過環境變數或密鑰管理進行管理。',
136
+ },
137
+ {
138
+ id: 'SECRETS-GENERIC-API-KEY-ENV',
139
+ pattern: /^(?:API_KEY|API_SECRET|SECRET_KEY|PRIVATE_KEY|ACCESS_TOKEN|AUTH_TOKEN|OAUTH_TOKEN|APP_SECRET)\s*=\s*[^\s#]{8,}/gm,
140
+ title: 'Generic API key or secret in environment file / 環境檔案中的通用 API 金鑰或密鑰',
141
+ description: 'A generic API key, secret, or token was found hardcoded in a configuration or environment file. ' +
142
+ 'These values should be injected at runtime, not stored in files. / ' +
143
+ '在設定或環境檔案中發現硬編碼的通用 API 金鑰、密鑰或令牌。這些值應在執行時注入,而非儲存在檔案中。',
144
+ },
145
+ ];
146
+ /**
147
+ * Remediation message for all secret findings (shared)
148
+ * 所有密鑰發現的修復建議(共用)
149
+ */
150
+ const SECRETS_REMEDIATION = 'Remove the secret from source code immediately. Rotate the compromised credential. ' +
151
+ 'Store secrets in environment variables or a secrets manager (e.g., AWS Secrets Manager, HashiCorp Vault). ' +
152
+ 'Add the file to .gitignore if it contains environment configuration. / ' +
153
+ '立即從原始碼中移除密鑰。輪換已洩露的憑證。' +
154
+ '將密鑰儲存在環境變數或密鑰管理員中(如 AWS Secrets Manager、HashiCorp Vault)。' +
155
+ '如果檔案包含環境設定,請將其加入 .gitignore。';
156
+ /**
157
+ * Determine if a file should be scanned based on its name and extension
158
+ * 根據檔案名稱和副檔名決定是否應掃描
159
+ *
160
+ * @param fileName - File name to check / 要檢查的檔案名稱
161
+ * @returns True if the file should be scanned / 若應掃描則為 true
162
+ */
163
+ function shouldScanFile(fileName) {
164
+ // Always scan .env files and variants
165
+ // 始終掃描 .env 檔案及其變體
166
+ if (/^\.env(\.|$)/.test(fileName))
167
+ return true;
168
+ const ext = path.extname(fileName).toLowerCase();
169
+ return SCANNABLE_EXTENSIONS.has(ext);
170
+ }
171
+ /**
172
+ * Recursively collect all files that should be scanned for secrets
173
+ * 遞迴收集所有應掃描密鑰的檔案
174
+ *
175
+ * @param dir - Directory to walk / 要遍歷的目錄
176
+ * @returns Array of absolute file paths / 絕對檔案路徑陣列
177
+ */
178
+ async function collectFilesForSecretScan(dir) {
179
+ const files = [];
180
+ async function walk(current) {
181
+ let entries;
182
+ try {
183
+ entries = await fs.readdir(current, { withFileTypes: true, encoding: 'utf-8' });
184
+ }
185
+ catch (err) {
186
+ logger.debug(`Cannot read directory: ${current}`, {
187
+ error: err instanceof Error ? err.message : String(err),
188
+ });
189
+ return;
190
+ }
191
+ for (const entry of entries) {
192
+ const fullPath = path.join(current, entry.name);
193
+ if (entry.isDirectory()) {
194
+ if (SKIP_DIRS.has(entry.name)) {
195
+ logger.debug(`Skipping directory: ${fullPath}`);
196
+ continue;
197
+ }
198
+ await walk(fullPath);
199
+ }
200
+ else if (entry.isFile()) {
201
+ if (!shouldScanFile(entry.name))
202
+ continue;
203
+ try {
204
+ const stat = await fs.stat(fullPath);
205
+ if (stat.size > MAX_FILE_SIZE_BYTES) {
206
+ logger.debug(`Skipping large file (${stat.size} bytes): ${fullPath}`);
207
+ continue;
208
+ }
209
+ files.push(fullPath);
210
+ }
211
+ catch {
212
+ logger.debug(`Cannot stat file: ${fullPath}`);
213
+ }
214
+ }
215
+ }
216
+ }
217
+ await walk(dir);
218
+ return files;
219
+ }
220
+ /**
221
+ * Scan a single file for hardcoded secrets
222
+ * 掃描單個檔案中的硬編碼密鑰
223
+ *
224
+ * @param filePath - Absolute path to the file / 檔案的絕對路徑
225
+ * @param content - File content / 檔案內容
226
+ * @returns Array of secret findings / 密鑰發現陣列
227
+ */
228
+ function scanFileForSecrets(filePath, content) {
229
+ const findings = [];
230
+ const seen = new Set();
231
+ for (const secretPattern of SECRET_PATTERNS) {
232
+ // Reset lastIndex for global regexes
233
+ // 重置全域正規表示式的 lastIndex
234
+ secretPattern.pattern.lastIndex = 0;
235
+ let match;
236
+ while ((match = secretPattern.pattern.exec(content)) !== null) {
237
+ const lineNum = content.slice(0, match.index).split('\n').length;
238
+ const matchedSnippet = match[0].slice(0, 40);
239
+ const dedupeKey = `${secretPattern.id}:${filePath}:${lineNum}`;
240
+ if (seen.has(dedupeKey))
241
+ continue;
242
+ seen.add(dedupeKey);
243
+ findings.push({
244
+ id: `${secretPattern.id}-${lineNum}`,
245
+ title: secretPattern.title,
246
+ description: `${secretPattern.description}\n\n` +
247
+ `File: ${filePath}, Line: ${lineNum}\n` +
248
+ `Found: ${matchedSnippet}${match[0].length > 40 ? '...' : ''}`,
249
+ severity: 'critical',
250
+ category: 'secrets',
251
+ remediation: SECRETS_REMEDIATION,
252
+ complianceRef: '4.5',
253
+ details: `${filePath}:${lineNum}`,
254
+ });
255
+ // For non-global patterns, break after first match per file
256
+ // 對於非全域模式,每個檔案匹配到第一個後中斷
257
+ if (!secretPattern.pattern.flags.includes('g'))
258
+ break;
259
+ }
260
+ // Reset lastIndex after each file
261
+ // 每個檔案後重置 lastIndex
262
+ secretPattern.pattern.lastIndex = 0;
263
+ }
264
+ return findings;
265
+ }
266
+ /**
267
+ * Scan source code directory for hardcoded secrets
268
+ * 掃描原始碼目錄中的硬編碼密鑰
269
+ *
270
+ * Detects common secret patterns including:
271
+ * - AWS Access Key IDs
272
+ * - GitHub tokens (ghp_, gho_, ghs_, ghr_ prefixes)
273
+ * - Slack tokens (xox[pboa]-)
274
+ * - Stripe live keys (sk_live_, pk_live_)
275
+ * - Bearer tokens in hardcoded strings
276
+ * - Private key headers (RSA, EC, etc.)
277
+ * - Database connection strings with credentials
278
+ * - Generic API keys in environment files
279
+ *
280
+ * 偵測常見的密鑰模式包括:
281
+ * - AWS 存取金鑰 ID
282
+ * - GitHub 令牌(ghp_、gho_、ghs_、ghr_ 前綴)
283
+ * - Slack 令牌(xox[pboa]-)
284
+ * - Stripe 正式金鑰(sk_live_、pk_live_)
285
+ * - 硬編碼字串中的 Bearer 令牌
286
+ * - 私鑰標頭(RSA、EC 等)
287
+ * - 含憑證的資料庫連線字串
288
+ * - 環境檔案中的通用 API 金鑰
289
+ *
290
+ * @param targetDir - Source code directory to scan / 要掃描的原始碼目錄
291
+ * @returns Array of secret findings / 密鑰發現陣列
292
+ */
293
+ export async function checkHardcodedSecrets(targetDir) {
294
+ // Validate targetDir exists
295
+ // 驗證 targetDir 存在
296
+ try {
297
+ const stat = await fs.stat(targetDir);
298
+ if (!stat.isDirectory()) {
299
+ logger.warn(`Target path is not a directory: ${targetDir}`);
300
+ return [];
301
+ }
302
+ }
303
+ catch (err) {
304
+ logger.warn(`Target directory does not exist or is not accessible: ${targetDir}`, {
305
+ error: err instanceof Error ? err.message : String(err),
306
+ });
307
+ return [];
308
+ }
309
+ const resolvedDir = path.resolve(targetDir);
310
+ logger.info('Starting hardcoded secrets scan', { targetDir: resolvedDir });
311
+ const files = await collectFilesForSecretScan(resolvedDir);
312
+ logger.info(`Secrets scanner: found ${files.length} file(s) to scan`);
313
+ const allFindings = [];
314
+ for (const filePath of files) {
315
+ try {
316
+ const content = await fs.readFile(filePath, 'utf-8');
317
+ const findings = scanFileForSecrets(filePath, content);
318
+ if (findings.length > 0) {
319
+ logger.info(`Found ${findings.length} secret(s) in: ${filePath}`);
320
+ allFindings.push(...findings);
321
+ }
322
+ }
323
+ catch (err) {
324
+ logger.debug(`Cannot read file: ${filePath}`, {
325
+ error: err instanceof Error ? err.message : String(err),
326
+ });
327
+ }
328
+ }
329
+ logger.info(`Secrets scan complete: ${allFindings.length} finding(s)`);
330
+ return allFindings;
331
+ }
332
+ //# sourceMappingURL=secrets-checker.js.map