@panguard-ai/panguard-scan 0.1.1 → 0.2.1
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 +1 -1
- package/dist/cli/commands.d.ts.map +1 -1
- package/dist/cli/commands.js +2 -1
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli/index.js +114 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/scanners/index.d.ts +3 -1
- package/dist/scanners/index.d.ts.map +1 -1
- package/dist/scanners/index.js +20 -1
- package/dist/scanners/index.js.map +1 -1
- package/dist/scanners/remote/index.d.ts.map +1 -1
- package/dist/scanners/remote/index.js +40 -0
- package/dist/scanners/remote/index.js.map +1 -1
- package/dist/scanners/sast-checker.d.ts +26 -0
- package/dist/scanners/sast-checker.d.ts.map +1 -0
- package/dist/scanners/sast-checker.js +289 -0
- package/dist/scanners/sast-checker.js.map +1 -0
- package/dist/scanners/secrets-checker.d.ts +41 -0
- package/dist/scanners/secrets-checker.d.ts.map +1 -0
- package/dist/scanners/secrets-checker.js +332 -0
- package/dist/scanners/secrets-checker.js.map +1 -0
- package/dist/scanners/types.d.ts +10 -0
- package/dist/scanners/types.d.ts.map +1 -1
- package/dist/scanners/types.js.map +1 -1
- package/package.json +13 -3
|
@@ -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":"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"}
|
|
@@ -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
|