ultraenv 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +2058 -0
- package/bin/ultraenv.mjs +3 -0
- package/dist/chunk-2USZPWLZ.js +288 -0
- package/dist/chunk-3UV2QNJL.js +270 -0
- package/dist/chunk-3VYXPTYV.js +179 -0
- package/dist/chunk-4XUYMRK5.js +366 -0
- package/dist/chunk-5G2DU52U.js +189 -0
- package/dist/chunk-6KS56D6E.js +172 -0
- package/dist/chunk-AWN6ADV7.js +328 -0
- package/dist/chunk-CHVO6NWI.js +203 -0
- package/dist/chunk-CIFMBJ4H.js +3975 -0
- package/dist/chunk-GC7RXHLA.js +253 -0
- package/dist/chunk-HFXQGJY3.js +445 -0
- package/dist/chunk-IGFVP24Q.js +91 -0
- package/dist/chunk-IKPTKALB.js +78 -0
- package/dist/chunk-JB7RKV3C.js +66 -0
- package/dist/chunk-MNVFG7H4.js +611 -0
- package/dist/chunk-MSXMESFP.js +1910 -0
- package/dist/chunk-N5PAV4NM.js +127 -0
- package/dist/chunk-NBOABPHM.js +158 -0
- package/dist/chunk-OMAOROL4.js +49 -0
- package/dist/chunk-R7PZRSZ7.js +105 -0
- package/dist/chunk-TE7HPLA6.js +73 -0
- package/dist/chunk-TMT5KCO3.js +101 -0
- package/dist/chunk-UEWYFN6A.js +189 -0
- package/dist/chunk-WMHN5RW2.js +128 -0
- package/dist/chunk-XC65ORJ5.js +70 -0
- package/dist/chunk-YMMP4VQL.js +118 -0
- package/dist/chunk-YN2KGTCB.js +33 -0
- package/dist/chunk-YTICOB5M.js +65 -0
- package/dist/chunk-YVWLXFUT.js +107 -0
- package/dist/ci-check-sync-VBMSVWIV.js +48 -0
- package/dist/ci-scan-24MT5XGS.js +41 -0
- package/dist/ci-setup-C2NKEFRD.js +135 -0
- package/dist/ci-validate-7AW24LSQ.js +57 -0
- package/dist/cli/index.cjs +9217 -0
- package/dist/cli/index.d.cts +9 -0
- package/dist/cli/index.d.ts +9 -0
- package/dist/cli/index.js +339 -0
- package/dist/comparator-RDKX3OI7.js +13 -0
- package/dist/completion-MW35C2XO.js +168 -0
- package/dist/config-O5YRQP5Z.js +13 -0
- package/dist/debug-PTPXAF3K.js +131 -0
- package/dist/declaration-LEME4AFZ.js +10 -0
- package/dist/doctor-FZAUPKHS.js +129 -0
- package/dist/envs-compare-5K3HESX5.js +49 -0
- package/dist/envs-create-2XXHXMGA.js +58 -0
- package/dist/envs-list-NQM5252B.js +59 -0
- package/dist/envs-switch-6L2AQYID.js +50 -0
- package/dist/envs-validate-FL73Q76T.js +89 -0
- package/dist/fs-VH7ATUS3.js +31 -0
- package/dist/generator-LFZBMZZS.js +14 -0
- package/dist/git-BZS4DPAI.js +30 -0
- package/dist/help-3XJBXEHE.js +121 -0
- package/dist/index.cjs +12907 -0
- package/dist/index.d.cts +2562 -0
- package/dist/index.d.ts +2562 -0
- package/dist/index.js +3212 -0
- package/dist/init-Y7JQ2KYJ.js +146 -0
- package/dist/install-hook-SKXIV6NV.js +111 -0
- package/dist/json-schema-I26YNQBH.js +10 -0
- package/dist/key-manager-O3G55WPU.js +25 -0
- package/dist/middleware/express.cjs +103 -0
- package/dist/middleware/express.d.cts +115 -0
- package/dist/middleware/express.d.ts +115 -0
- package/dist/middleware/express.js +8 -0
- package/dist/middleware/fastify.cjs +91 -0
- package/dist/middleware/fastify.d.cts +111 -0
- package/dist/middleware/fastify.d.ts +111 -0
- package/dist/middleware/fastify.js +8 -0
- package/dist/module-IDIZPP4M.js +10 -0
- package/dist/protect-NCWPM6VC.js +161 -0
- package/dist/scan-TRLY36TT.js +58 -0
- package/dist/schema/index.cjs +4074 -0
- package/dist/schema/index.d.cts +1244 -0
- package/dist/schema/index.d.ts +1244 -0
- package/dist/schema/index.js +152 -0
- package/dist/sync-TMHMTLH2.js +186 -0
- package/dist/typegen-SQOSXBWM.js +80 -0
- package/dist/validate-IOAM5HWS.js +100 -0
- package/dist/vault-decrypt-U6HJZNBV.js +111 -0
- package/dist/vault-diff-B3ZOQTWI.js +132 -0
- package/dist/vault-encrypt-GUSLCSKS.js +112 -0
- package/dist/vault-init-GUBOTOUL.js +106 -0
- package/dist/vault-rekey-DAHT7JCN.js +132 -0
- package/dist/vault-status-GDLRU2OK.js +90 -0
- package/dist/vault-verify-CD76FJSF.js +102 -0
- package/package.json +106 -0
|
@@ -0,0 +1,1910 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getStagedFiles,
|
|
3
|
+
isGitIgnored,
|
|
4
|
+
isGitRepository
|
|
5
|
+
} from "./chunk-R7PZRSZ7.js";
|
|
6
|
+
import {
|
|
7
|
+
maskValue
|
|
8
|
+
} from "./chunk-TE7HPLA6.js";
|
|
9
|
+
|
|
10
|
+
// src/data/secret-patterns.ts
|
|
11
|
+
var SECRET_PATTERNS = [
|
|
12
|
+
// =========================================================================
|
|
13
|
+
// AWS
|
|
14
|
+
// =========================================================================
|
|
15
|
+
{
|
|
16
|
+
id: "aws-access-key-id",
|
|
17
|
+
name: "AWS Access Key ID",
|
|
18
|
+
pattern: /(?:^|["'\s:=,`])(AKIA[0-9A-Z]{16})(?:["'\s,`]|$)/gm,
|
|
19
|
+
confidence: 0.95,
|
|
20
|
+
severity: "critical",
|
|
21
|
+
description: "AWS IAM Access Key ID. Grants programmatic access to AWS resources.",
|
|
22
|
+
remediation: "Rotate the key immediately in the AWS IAM Console. Use environment variables or a secrets manager. Never commit access keys to source control.",
|
|
23
|
+
category: "aws"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: "aws-secret-access-key",
|
|
27
|
+
name: "AWS Secret Access Key",
|
|
28
|
+
pattern: /(?:^|["'\s:=,`])(aws(.{0,20})?(secret|Secret|SECRET)(.{0,20})?[=\s]\s*["']?)([A-Za-z0-9/+=]{40})(?:["'\s,`;]|$)/gm,
|
|
29
|
+
confidence: 0.85,
|
|
30
|
+
severity: "critical",
|
|
31
|
+
description: "AWS Secret Access Key. The secret counterpart to an AWS Access Key ID.",
|
|
32
|
+
remediation: "Rotate the key immediately. Use AWS IAM to generate new credentials and store them in a secure vault.",
|
|
33
|
+
category: "aws"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: "aws-session-token",
|
|
37
|
+
name: "AWS Session Token",
|
|
38
|
+
pattern: /(?:^|["'\s:=,`])(AQo[A-Za-z0-9/+=]{150,}(?:%3D){0,2})(?:["'\s,`;]|$)/gm,
|
|
39
|
+
confidence: 0.9,
|
|
40
|
+
severity: "critical",
|
|
41
|
+
description: "AWS temporary session token (STS). Grants short-lived AWS credentials.",
|
|
42
|
+
remediation: "Do not commit session tokens. They expire but should still be treated as sensitive. Use role-based access instead.",
|
|
43
|
+
category: "aws"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: "aws-account-id",
|
|
47
|
+
name: "AWS Account ID",
|
|
48
|
+
pattern: /(?:^|["'\s:=,(])(\d{12})(?:["'\s,)]|$)/gm,
|
|
49
|
+
confidence: 0.3,
|
|
50
|
+
severity: "medium",
|
|
51
|
+
description: "Potential AWS Account ID (12-digit number). May indicate hardcoded AWS resource references.",
|
|
52
|
+
remediation: "Use environment variables or parameter store for account IDs. Avoid hardcoding account-specific identifiers.",
|
|
53
|
+
category: "aws"
|
|
54
|
+
},
|
|
55
|
+
// =========================================================================
|
|
56
|
+
// GitHub
|
|
57
|
+
// =========================================================================
|
|
58
|
+
{
|
|
59
|
+
id: "github-personal-access-token",
|
|
60
|
+
name: "GitHub Personal Access Token",
|
|
61
|
+
pattern: /(?:^|["'\s:=,`])(ghp_[A-Za-z0-9_]{36,})(?:["'\s,`;]|$)/gm,
|
|
62
|
+
confidence: 0.95,
|
|
63
|
+
severity: "critical",
|
|
64
|
+
description: "GitHub Personal Access Token (classic). Grants access to GitHub APIs and repositories.",
|
|
65
|
+
remediation: "Revoke the token immediately at GitHub Settings > Developer settings > Personal access tokens. Use GitHub Secrets or a vault.",
|
|
66
|
+
category: "github"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "github-oauth-access-token",
|
|
70
|
+
name: "GitHub OAuth Access Token",
|
|
71
|
+
pattern: /(?:^|["'\s:=,`])(gho_[A-Za-z0-9_]{36,})(?:["'\s,`;]|$)/gm,
|
|
72
|
+
confidence: 0.95,
|
|
73
|
+
severity: "critical",
|
|
74
|
+
description: "GitHub OAuth Access Token. Used for OAuth-based GitHub authentication.",
|
|
75
|
+
remediation: "Revoke the OAuth application at GitHub Settings > Developer settings > OAuth Apps. Rotate the token.",
|
|
76
|
+
category: "github"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: "github-app-token",
|
|
80
|
+
name: "GitHub App Token",
|
|
81
|
+
pattern: /(?:^|["'\s:=,`])(ghs_[A-Za-z0-9_]{36,})(?:["'\s,`;]|$)/gm,
|
|
82
|
+
confidence: 0.95,
|
|
83
|
+
severity: "critical",
|
|
84
|
+
description: "GitHub App Installation Token. Grants repository-scoped access via a GitHub App.",
|
|
85
|
+
remediation: "Revoke the app installation and generate a new token. Store tokens in a secure vault with short TTL.",
|
|
86
|
+
category: "github"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: "github-refresh-token",
|
|
90
|
+
name: "GitHub Refresh Token",
|
|
91
|
+
pattern: /(?:^|["'\s:=,`])(ghr_[A-Za-z0-9_]{36,})(?:["'\s,`;]|$)/gm,
|
|
92
|
+
confidence: 0.95,
|
|
93
|
+
severity: "critical",
|
|
94
|
+
description: "GitHub Refresh Token. Used to obtain new user-to-server tokens.",
|
|
95
|
+
remediation: "Revoke the refresh token at GitHub Settings > Developer settings. Regenerate and store securely.",
|
|
96
|
+
category: "github"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: "github-user-to-server-token",
|
|
100
|
+
name: "GitHub User-to-Server Token",
|
|
101
|
+
pattern: /(?:^|["'\s:=,`])(ghu_[A-Za-z0-9_]{36,})(?:["'\s,`;]|$)/gm,
|
|
102
|
+
confidence: 0.95,
|
|
103
|
+
severity: "critical",
|
|
104
|
+
description: "GitHub User-to-Server Token. Grants a user access to a GitHub App.",
|
|
105
|
+
remediation: "Revoke the token at GitHub Settings > Developer settings. Use GitHub Secrets for storage.",
|
|
106
|
+
category: "github"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: "github-webhook-secret",
|
|
110
|
+
name: "GitHub Webhook Secret",
|
|
111
|
+
pattern: /(?:^|["'\s:=,`])(ghw_[A-Za-z0-9_]{36,})(?:["'\s,`;]|$)/gm,
|
|
112
|
+
confidence: 0.9,
|
|
113
|
+
severity: "high",
|
|
114
|
+
description: "GitHub Webhook Secret. Used to verify the authenticity of webhook payloads.",
|
|
115
|
+
remediation: "Regenerate the webhook secret in repository settings. Store in a secure vault. Never commit to source.",
|
|
116
|
+
category: "github"
|
|
117
|
+
},
|
|
118
|
+
// =========================================================================
|
|
119
|
+
// Stripe
|
|
120
|
+
// =========================================================================
|
|
121
|
+
{
|
|
122
|
+
id: "stripe-secret-key",
|
|
123
|
+
name: "Stripe Secret Key",
|
|
124
|
+
pattern: /(?:^|["'\s:=,`])(sk_live_[A-Za-z0-9]{24,})(?:["'\s,`;]|$)/gm,
|
|
125
|
+
confidence: 0.95,
|
|
126
|
+
severity: "critical",
|
|
127
|
+
description: "Stripe Live Secret Key. Can process live payments and access customer data.",
|
|
128
|
+
remediation: "Roll the API key immediately in the Stripe Dashboard. Use environment variables and never commit to source.",
|
|
129
|
+
category: "stripe"
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: "stripe-publishable-key",
|
|
133
|
+
name: "Stripe Publishable Key",
|
|
134
|
+
pattern: /(?:^|["'\s:=,`])(pk_live_[A-Za-z0-9]{24,})((?:["'\s,`;]|$))/gm,
|
|
135
|
+
confidence: 0.7,
|
|
136
|
+
severity: "medium",
|
|
137
|
+
description: "Stripe Live Publishable Key. Designed to be public but should not be hardcoded.",
|
|
138
|
+
remediation: "Use environment variables for publishable keys to enable environment-specific configuration.",
|
|
139
|
+
category: "stripe"
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: "stripe-restricted-key",
|
|
143
|
+
name: "Stripe Restricted Key",
|
|
144
|
+
pattern: /(?:^|["'\s:=,`])(rk_live_[A-Za-z0-9]{24,})(?:["'\s,`;]|$)/gm,
|
|
145
|
+
confidence: 0.95,
|
|
146
|
+
severity: "critical",
|
|
147
|
+
description: "Stripe Restricted Key. Scoped API key with limited permissions for live mode.",
|
|
148
|
+
remediation: "Roll the restricted key in the Stripe Dashboard. Store in a vault with proper access controls.",
|
|
149
|
+
category: "stripe"
|
|
150
|
+
},
|
|
151
|
+
// =========================================================================
|
|
152
|
+
// Slack
|
|
153
|
+
// =========================================================================
|
|
154
|
+
{
|
|
155
|
+
id: "slack-bot-token",
|
|
156
|
+
name: "Slack Bot Token",
|
|
157
|
+
pattern: /(?:^|["'\s:=,`])(xoxb-[A-Za-z0-9-]{10,})(?:["'\s,`;]|$)/gm,
|
|
158
|
+
confidence: 0.9,
|
|
159
|
+
severity: "critical",
|
|
160
|
+
description: "Slack Bot Token (xoxb-). Grants API access to Slack workspace.",
|
|
161
|
+
remediation: "Revoke the bot token at api.slack.com/apps. Rotate and store in a secrets manager.",
|
|
162
|
+
category: "slack"
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: "slack-user-token",
|
|
166
|
+
name: "Slack User Token",
|
|
167
|
+
pattern: /(?:^|["'\s:=,`])(xoxp-[A-Za-z0-9-]{10,})(?:["'\s,`;]|$)/gm,
|
|
168
|
+
confidence: 0.9,
|
|
169
|
+
severity: "critical",
|
|
170
|
+
description: "Slack User Token (xoxp-). Grants user-level Slack workspace access.",
|
|
171
|
+
remediation: "Revoke the user token immediately. Rotate and store in a secure vault.",
|
|
172
|
+
category: "slack"
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: "slack-app-token",
|
|
176
|
+
name: "Slack App-Level Token",
|
|
177
|
+
pattern: /(?:^|["'\s:=,`])(xoxa-[A-Za-z0-9-]{10,})(?:["'\s,`;]|$)/gm,
|
|
178
|
+
confidence: 0.9,
|
|
179
|
+
severity: "critical",
|
|
180
|
+
description: "Slack App-Level Token (xoxa-). Grants app-level Slack access.",
|
|
181
|
+
remediation: "Revoke the token in the Slack API app management dashboard. Rotate credentials.",
|
|
182
|
+
category: "slack"
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
id: "slack-webhook-url",
|
|
186
|
+
name: "Slack Webhook URL",
|
|
187
|
+
pattern: /https:\/\/hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[A-Za-z0-9]+/g,
|
|
188
|
+
confidence: 0.9,
|
|
189
|
+
severity: "high",
|
|
190
|
+
description: "Slack Incoming Webhook URL. Can be used to post messages to Slack channels.",
|
|
191
|
+
remediation: "Rotate the webhook URL in your Slack app configuration. Use environment variables.",
|
|
192
|
+
category: "slack"
|
|
193
|
+
},
|
|
194
|
+
// =========================================================================
|
|
195
|
+
// Google
|
|
196
|
+
// =========================================================================
|
|
197
|
+
{
|
|
198
|
+
id: "google-api-key",
|
|
199
|
+
name: "Google API Key",
|
|
200
|
+
pattern: /(?:^|["'\s:=,`])(AIza[0-9A-Za-z_-]{35})(?:["'\s,`;]|$)/gm,
|
|
201
|
+
confidence: 0.85,
|
|
202
|
+
severity: "high",
|
|
203
|
+
description: "Google API Key. Grants access to Google Cloud Platform services.",
|
|
204
|
+
remediation: "Restrict the API key in Google Cloud Console > APIs & Services > Credentials. Set application restrictions and API restrictions.",
|
|
205
|
+
category: "google"
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
id: "google-oauth-client-id",
|
|
209
|
+
name: "Google OAuth Client ID",
|
|
210
|
+
pattern: /(?:^|["'\s:=,`])(\d{4,}-[a-z0-9_]{32}\.apps\.googleusercontent\.com)(?:["'\s,`;]|$)/gm,
|
|
211
|
+
confidence: 0.85,
|
|
212
|
+
severity: "medium",
|
|
213
|
+
description: "Google OAuth Client ID. Identifies the application for OAuth flows.",
|
|
214
|
+
remediation: "Restrict the OAuth client to specific domains in Google Cloud Console. Store in environment variables.",
|
|
215
|
+
category: "google"
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
id: "google-oauth-client-secret",
|
|
219
|
+
name: "Google OAuth Client Secret",
|
|
220
|
+
pattern: /(?:^|["'\s:=,`]GOCSPX-[A-Za-z0-9_-]{28,})(?:["'\s,`;]|$)/gm,
|
|
221
|
+
confidence: 0.9,
|
|
222
|
+
severity: "critical",
|
|
223
|
+
description: "Google OAuth Client Secret (new format). Used for OAuth authentication flows.",
|
|
224
|
+
remediation: "Reset the client secret in Google Cloud Console. Store in a secrets manager.",
|
|
225
|
+
category: "google"
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
id: "google-firebase-api-key",
|
|
229
|
+
name: "Google Firebase API Key",
|
|
230
|
+
pattern: /(?:^|["'\s:=,`])(AAAA[A-Za-z0-9_-]{36,})(?:["'\s,`;]|$)/gm,
|
|
231
|
+
confidence: 0.8,
|
|
232
|
+
severity: "high",
|
|
233
|
+
description: "Google Firebase Cloud Messaging API Key (Server Key / Legacy).",
|
|
234
|
+
remediation: "Migrate to Firebase Cloud Messaging v1 API. Use service account credentials instead of legacy API keys.",
|
|
235
|
+
category: "google"
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
id: "google-service-account-private-key",
|
|
239
|
+
name: "Google Service Account Private Key",
|
|
240
|
+
pattern: /"private_key"\s*:\s*"(-----BEGIN (?:RSA |EC )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC )?PRIVATE KEY-----)"/gm,
|
|
241
|
+
confidence: 0.95,
|
|
242
|
+
severity: "critical",
|
|
243
|
+
description: "Google Cloud Service Account private key embedded in JSON credentials.",
|
|
244
|
+
remediation: "Rotate the service account key in Google Cloud IAM. Use Workload Identity or ADC (Application Default Credentials) instead.",
|
|
245
|
+
category: "google"
|
|
246
|
+
},
|
|
247
|
+
// =========================================================================
|
|
248
|
+
// Private Keys & Certificates
|
|
249
|
+
// =========================================================================
|
|
250
|
+
{
|
|
251
|
+
id: "rsa-private-key",
|
|
252
|
+
name: "RSA Private Key",
|
|
253
|
+
pattern: /-----BEGIN RSA PRIVATE KEY-----[\s\S]*?-----END RSA PRIVATE KEY-----/gm,
|
|
254
|
+
confidence: 0.99,
|
|
255
|
+
severity: "critical",
|
|
256
|
+
description: "RSA Private Key in PEM format. Used for TLS, SSH, or code signing.",
|
|
257
|
+
remediation: "Remove the private key from source code. Generate a new key pair and store the private key in a hardware security module or vault.",
|
|
258
|
+
category: "keys"
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
id: "ec-private-key",
|
|
262
|
+
name: "EC Private Key",
|
|
263
|
+
pattern: /-----BEGIN EC PRIVATE KEY-----[\s\S]*?-----END EC PRIVATE KEY-----/gm,
|
|
264
|
+
confidence: 0.99,
|
|
265
|
+
severity: "critical",
|
|
266
|
+
description: "Elliptic Curve Private Key in PEM format.",
|
|
267
|
+
remediation: "Remove from source code. Generate a new key pair and store the private key in a secure vault.",
|
|
268
|
+
category: "keys"
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
id: "dsa-private-key",
|
|
272
|
+
name: "DSA Private Key",
|
|
273
|
+
pattern: /-----BEGIN DSA PRIVATE KEY-----[\s\S]*?-----END DSA PRIVATE KEY-----/gm,
|
|
274
|
+
confidence: 0.99,
|
|
275
|
+
severity: "critical",
|
|
276
|
+
description: "DSA Private Key in PEM format.",
|
|
277
|
+
remediation: "Remove from source code. Generate a new key and store in a secure vault.",
|
|
278
|
+
category: "keys"
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
id: "openssh-private-key",
|
|
282
|
+
name: "OpenSSH Private Key",
|
|
283
|
+
pattern: /-----BEGIN OPENSSH PRIVATE KEY-----[\s\S]*?-----END OPENSSH PRIVATE KEY-----/gm,
|
|
284
|
+
confidence: 0.99,
|
|
285
|
+
severity: "critical",
|
|
286
|
+
description: "OpenSSH Private Key. Grants SSH access to remote systems.",
|
|
287
|
+
remediation: "Remove from source code. Generate a new SSH key pair and store the private key securely.",
|
|
288
|
+
category: "keys"
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
id: "pgp-private-key",
|
|
292
|
+
name: "PGP Private Key",
|
|
293
|
+
pattern: /-----BEGIN PGP PRIVATE KEY BLOCK-----[\s\S]*?-----END PGP PRIVATE KEY BLOCK-----/gm,
|
|
294
|
+
confidence: 0.99,
|
|
295
|
+
severity: "critical",
|
|
296
|
+
description: "PGP Private Key Block. Used for encryption and signing.",
|
|
297
|
+
remediation: "Remove from source code. Revoke the key if necessary and generate a new one.",
|
|
298
|
+
category: "keys"
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
id: "encrypted-private-key",
|
|
302
|
+
name: "Encrypted Private Key",
|
|
303
|
+
pattern: /-----BEGIN ENCRYPTED PRIVATE KEY-----[\s\S]*?-----END ENCRYPTED PRIVATE KEY-----/gm,
|
|
304
|
+
confidence: 0.95,
|
|
305
|
+
severity: "critical",
|
|
306
|
+
description: "Encrypted Private Key in PEM format. Still sensitive even with encryption.",
|
|
307
|
+
remediation: "Remove from source code. Store in a dedicated secrets manager.",
|
|
308
|
+
category: "keys"
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
id: "pem-certificate",
|
|
312
|
+
name: "PEM Certificate",
|
|
313
|
+
pattern: /-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/gm,
|
|
314
|
+
confidence: 0.85,
|
|
315
|
+
severity: "high",
|
|
316
|
+
description: "PEM-encoded X.509 Certificate. May contain sensitive organizational information.",
|
|
317
|
+
remediation: "Remove from source code. Store certificates in a certificate management system.",
|
|
318
|
+
category: "keys"
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
id: "pkcs8-private-key",
|
|
322
|
+
name: "PKCS#8 Private Key",
|
|
323
|
+
pattern: /-----BEGIN PRIVATE KEY-----[\s\S]*?-----END PRIVATE KEY-----/gm,
|
|
324
|
+
confidence: 0.99,
|
|
325
|
+
severity: "critical",
|
|
326
|
+
description: "PKCS#8 Private Key in PEM format (generic unencrypted private key).",
|
|
327
|
+
remediation: "Remove from source code. Store in a hardware security module or vault.",
|
|
328
|
+
category: "keys"
|
|
329
|
+
},
|
|
330
|
+
// =========================================================================
|
|
331
|
+
// JWT Tokens
|
|
332
|
+
// =========================================================================
|
|
333
|
+
{
|
|
334
|
+
id: "jwt-token",
|
|
335
|
+
name: "JSON Web Token",
|
|
336
|
+
pattern: /(?:^|["'\s:=,`])(eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,})(?:["'\s,`;]|$)/gm,
|
|
337
|
+
confidence: 0.75,
|
|
338
|
+
severity: "high",
|
|
339
|
+
description: "JSON Web Token (JWT). May contain sensitive claims and grant authentication.",
|
|
340
|
+
remediation: "Do not hardcode JWTs. Use secure token exchange mechanisms and short-lived tokens.",
|
|
341
|
+
category: "auth"
|
|
342
|
+
},
|
|
343
|
+
// =========================================================================
|
|
344
|
+
// Database Connection Strings
|
|
345
|
+
// =========================================================================
|
|
346
|
+
{
|
|
347
|
+
id: "mongodb-connection-string",
|
|
348
|
+
name: "MongoDB Connection String",
|
|
349
|
+
pattern: /mongodb(?:\+srv)?:\/\/[^\s"'`]+/g,
|
|
350
|
+
confidence: 0.9,
|
|
351
|
+
severity: "critical",
|
|
352
|
+
description: "MongoDB connection URI containing credentials and connection details.",
|
|
353
|
+
remediation: "Store connection strings in a secrets manager. Use environment variables. Mask credentials in connection strings.",
|
|
354
|
+
category: "database"
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
id: "postgresql-connection-string",
|
|
358
|
+
name: "PostgreSQL Connection String",
|
|
359
|
+
pattern: /postgres(?:ql)?:\/\/[^\s"'`]+@[^\s"'`]+/g,
|
|
360
|
+
confidence: 0.9,
|
|
361
|
+
severity: "critical",
|
|
362
|
+
description: "PostgreSQL connection URI containing username and password.",
|
|
363
|
+
remediation: "Store connection strings in a secrets manager. Use IAM authentication when possible.",
|
|
364
|
+
category: "database"
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
id: "mysql-connection-string",
|
|
368
|
+
name: "MySQL Connection String",
|
|
369
|
+
pattern: /mysql(?:\+ssl)?:\/\/[^\s"'`]+@[^\s"'`]+/g,
|
|
370
|
+
confidence: 0.9,
|
|
371
|
+
severity: "critical",
|
|
372
|
+
description: "MySQL connection URI containing username and password.",
|
|
373
|
+
remediation: "Store connection strings in a secrets manager. Rotate database credentials regularly.",
|
|
374
|
+
category: "database"
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
id: "redis-connection-string",
|
|
378
|
+
name: "Redis Connection String",
|
|
379
|
+
pattern: /redis(?:\+sentinel)?(?:\+ssl)?:\/\/[^\s"'`]+/g,
|
|
380
|
+
confidence: 0.85,
|
|
381
|
+
severity: "critical",
|
|
382
|
+
description: "Redis connection URI with optional credentials.",
|
|
383
|
+
remediation: "Store connection strings in a secrets manager. Enable TLS and use ACLs for authentication.",
|
|
384
|
+
category: "database"
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
id: "couchdb-connection-string",
|
|
388
|
+
name: "CouchDB Connection String",
|
|
389
|
+
pattern: /couchdb(?:s)?:\/\/[^\s"'`]+@[^\s"'`]+/g,
|
|
390
|
+
confidence: 0.85,
|
|
391
|
+
severity: "critical",
|
|
392
|
+
description: "CouchDB connection URI containing credentials.",
|
|
393
|
+
remediation: "Store connection strings in a secrets manager. Use proxy authentication.",
|
|
394
|
+
category: "database"
|
|
395
|
+
},
|
|
396
|
+
// =========================================================================
|
|
397
|
+
// SendGrid
|
|
398
|
+
// =========================================================================
|
|
399
|
+
{
|
|
400
|
+
id: "sendgrid-api-key",
|
|
401
|
+
name: "SendGrid API Key",
|
|
402
|
+
pattern: /(?:^|["'\s:=,`])(SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43})(?:["'\s,`;]|$)/gm,
|
|
403
|
+
confidence: 0.95,
|
|
404
|
+
severity: "critical",
|
|
405
|
+
description: "SendGrid API Key. Grants access to send emails and manage contacts.",
|
|
406
|
+
remediation: "Revoke the key in SendGrid Dashboard > Settings > API Keys. Store new key in a secrets manager.",
|
|
407
|
+
category: "email"
|
|
408
|
+
},
|
|
409
|
+
// =========================================================================
|
|
410
|
+
// Twilio
|
|
411
|
+
// =========================================================================
|
|
412
|
+
{
|
|
413
|
+
id: "twilio-api-key-sid",
|
|
414
|
+
name: "Twilio API Key SID",
|
|
415
|
+
pattern: /(?:^|["'\s:=,`])(SK[0-9a-fA-F]{32})(?:["'\s,`;]|$)/gm,
|
|
416
|
+
confidence: 0.85,
|
|
417
|
+
severity: "critical",
|
|
418
|
+
description: "Twilio API Key SID. Used for authenticating Twilio API requests.",
|
|
419
|
+
remediation: "Rotate the API key in the Twilio Console. Store in a secrets manager.",
|
|
420
|
+
category: "telecom"
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
id: "twilio-auth-token",
|
|
424
|
+
name: "Twilio Auth Token",
|
|
425
|
+
pattern: /(?:twilio|TWILIO).{0,30}(?:auth|AUTH).{0,10}(?:token|TOKEN)[\s]*[:=][\s]*["']?([A-Za-z0-9]{32})(?:["'\s,`;]|$)/gm,
|
|
426
|
+
confidence: 0.85,
|
|
427
|
+
severity: "critical",
|
|
428
|
+
description: "Twilio Account Auth Token. Grants full access to the Twilio account.",
|
|
429
|
+
remediation: "Rotate the auth token in the Twilio Console. Never commit to source control.",
|
|
430
|
+
category: "telecom"
|
|
431
|
+
},
|
|
432
|
+
// =========================================================================
|
|
433
|
+
// Azure
|
|
434
|
+
// =========================================================================
|
|
435
|
+
{
|
|
436
|
+
id: "azure-connection-string",
|
|
437
|
+
name: "Azure Connection String",
|
|
438
|
+
pattern: /DefaultEndpointsProtocol=https?;AccountName=[A-Za-z0-9]+;AccountKey=[A-Za-z0-9+/=]+/g,
|
|
439
|
+
confidence: 0.95,
|
|
440
|
+
severity: "critical",
|
|
441
|
+
description: "Azure Storage Account connection string containing the account key.",
|
|
442
|
+
remediation: "Use Azure Key Vault or Managed Identity instead of connection strings. Rotate the account key.",
|
|
443
|
+
category: "azure"
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
id: "azure-credential",
|
|
447
|
+
name: "Azure Credentials",
|
|
448
|
+
pattern: /(?:^|["'\s:=,`])([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.[A-Za-z0-9_-]{30,})(?:["'\s,`;]|$)/gm,
|
|
449
|
+
confidence: 0.75,
|
|
450
|
+
severity: "critical",
|
|
451
|
+
description: "Potential Azure service principal credential or managed identity token.",
|
|
452
|
+
remediation: "Use Azure Key Vault for storing credentials. Rotate service principal secrets.",
|
|
453
|
+
category: "azure"
|
|
454
|
+
},
|
|
455
|
+
// =========================================================================
|
|
456
|
+
// DigitalOcean
|
|
457
|
+
// =========================================================================
|
|
458
|
+
{
|
|
459
|
+
id: "digitalocean-token",
|
|
460
|
+
name: "DigitalOcean API Token",
|
|
461
|
+
pattern: /(?:^|["'\s:=,`])(dop_v1_[A-Za-z0-9]{64,})(?:["'\s,`;]|$)/gm,
|
|
462
|
+
confidence: 0.95,
|
|
463
|
+
severity: "critical",
|
|
464
|
+
description: "DigitalOcean API Token (v1). Grants full API access.",
|
|
465
|
+
remediation: "Revoke the token in the DigitalOcean Control Panel > API > Tokens. Store in a secrets manager.",
|
|
466
|
+
category: "cloud"
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
id: "digitalocean-pat",
|
|
470
|
+
name: "DigitalOcean Personal Access Token",
|
|
471
|
+
pattern: /(?:^|["'\s:=,`])(dop_v1_[a-f0-9]{64})(?:["'\s,`;]|$)/gm,
|
|
472
|
+
confidence: 0.9,
|
|
473
|
+
severity: "critical",
|
|
474
|
+
description: "DigitalOcean Personal Access Token.",
|
|
475
|
+
remediation: "Revoke the token in DigitalOcean settings and generate a new one. Store securely.",
|
|
476
|
+
category: "cloud"
|
|
477
|
+
},
|
|
478
|
+
// =========================================================================
|
|
479
|
+
// Heroku
|
|
480
|
+
// =========================================================================
|
|
481
|
+
{
|
|
482
|
+
id: "heroku-api-key",
|
|
483
|
+
name: "Heroku API Key",
|
|
484
|
+
pattern: /(?:^|["'\s:=,`])([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-[a-f0-9]{16})(?:["'\s,`;]|$)/gm,
|
|
485
|
+
confidence: 0.7,
|
|
486
|
+
severity: "high",
|
|
487
|
+
description: "Heroku API Key. Grants access to Heroku platform APIs.",
|
|
488
|
+
remediation: "Rotate the API key in Heroku Account Settings. Store in environment variables.",
|
|
489
|
+
category: "cloud"
|
|
490
|
+
},
|
|
491
|
+
// =========================================================================
|
|
492
|
+
// NPM
|
|
493
|
+
// =========================================================================
|
|
494
|
+
{
|
|
495
|
+
id: "npm-token",
|
|
496
|
+
name: "NPM Access Token",
|
|
497
|
+
pattern: /(?:^|["'\s:=,`])(npm_[A-Za-z0-9]{36,})(?:["'\s,`;]|$)/gm,
|
|
498
|
+
confidence: 0.95,
|
|
499
|
+
severity: "critical",
|
|
500
|
+
description: "NPM Access Token. Can be used to publish packages to the npm registry.",
|
|
501
|
+
remediation: "Revoke the token at https://www.npmjs.com/settings/keys. Generate a new token with minimal scope.",
|
|
502
|
+
category: "package-manager"
|
|
503
|
+
},
|
|
504
|
+
// =========================================================================
|
|
505
|
+
// Docker
|
|
506
|
+
// =========================================================================
|
|
507
|
+
{
|
|
508
|
+
id: "docker-hub-credentials",
|
|
509
|
+
name: "Docker Hub Credentials",
|
|
510
|
+
pattern: /docker\.io\/[^\s"'`]+:[^\s"'`]+@[^\s"'`]+/g,
|
|
511
|
+
confidence: 0.85,
|
|
512
|
+
severity: "high",
|
|
513
|
+
description: "Docker Hub credentials embedded in a registry URL.",
|
|
514
|
+
remediation: "Use docker login with credentials stored securely. Use Docker credential helpers.",
|
|
515
|
+
category: "container"
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
id: "docker-auth-token",
|
|
519
|
+
name: "Docker Registry Auth Token",
|
|
520
|
+
pattern: /(?:^|["'\s:=,`])(eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,})(?:["'\s,`;]|$)/gm,
|
|
521
|
+
confidence: 0.7,
|
|
522
|
+
severity: "high",
|
|
523
|
+
description: "Docker Registry Bearer Token (JWT format). Used for container registry authentication.",
|
|
524
|
+
remediation: "Do not store Docker auth tokens in source. Use docker credential store or CI secrets.",
|
|
525
|
+
category: "container"
|
|
526
|
+
},
|
|
527
|
+
// =========================================================================
|
|
528
|
+
// Firebase
|
|
529
|
+
// =========================================================================
|
|
530
|
+
{
|
|
531
|
+
id: "firebase-config",
|
|
532
|
+
name: "Firebase Configuration",
|
|
533
|
+
pattern: /firebase[A-Za-z]*\.json["'\s]*[:=]\s*["'][\s\S]*?"apiKey"\s*:\s*"[A-Za-z0-9_-]+"/gm,
|
|
534
|
+
confidence: 0.8,
|
|
535
|
+
severity: "medium",
|
|
536
|
+
description: "Firebase configuration block containing API keys and project identifiers.",
|
|
537
|
+
remediation: "Use Firebase Admin SDK with service account credentials. Restrict API keys in Google Cloud Console.",
|
|
538
|
+
category: "google"
|
|
539
|
+
},
|
|
540
|
+
// =========================================================================
|
|
541
|
+
// Shopify
|
|
542
|
+
// =========================================================================
|
|
543
|
+
{
|
|
544
|
+
id: "shopify-app-secret",
|
|
545
|
+
name: "Shopify App Secret",
|
|
546
|
+
pattern: /(?:^|["'\s:=,`])(shpca_[A-Za-z0-9]{32,})(?:["'\s,`;]|$)/gm,
|
|
547
|
+
confidence: 0.9,
|
|
548
|
+
severity: "critical",
|
|
549
|
+
description: "Shopify App Client Secret. Used for OAuth with Shopify apps.",
|
|
550
|
+
remediation: "Regenerate the app secret in the Shopify Partners Dashboard. Store in a vault.",
|
|
551
|
+
category: "e-commerce"
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
id: "shopify-access-token",
|
|
555
|
+
name: "Shopify Access Token",
|
|
556
|
+
pattern: /(?:^|["'\s:=,`])(shpat_[A-Za-z0-9]{32,})(?:["'\s,`;]|$)/gm,
|
|
557
|
+
confidence: 0.9,
|
|
558
|
+
severity: "critical",
|
|
559
|
+
description: "Shopify App Access Token. Grants API access to a Shopify store.",
|
|
560
|
+
remediation: "Revoke the access token in the Shopify Admin. Regenerate and store securely.",
|
|
561
|
+
category: "e-commerce"
|
|
562
|
+
},
|
|
563
|
+
// =========================================================================
|
|
564
|
+
// Telegram
|
|
565
|
+
// =========================================================================
|
|
566
|
+
{
|
|
567
|
+
id: "telegram-bot-token",
|
|
568
|
+
name: "Telegram Bot Token",
|
|
569
|
+
pattern: /(?:^|["'\s:=,`])(\d{8,10}:[A-Za-z0-9_-]{35})(?:["'\s,`;]|$)/gm,
|
|
570
|
+
confidence: 0.85,
|
|
571
|
+
severity: "high",
|
|
572
|
+
description: "Telegram Bot API Token. Grants control over a Telegram bot.",
|
|
573
|
+
remediation: "Revoke the token by messaging @BotFather on Telegram. Generate a new token and store securely.",
|
|
574
|
+
category: "messaging"
|
|
575
|
+
},
|
|
576
|
+
// =========================================================================
|
|
577
|
+
// Discord
|
|
578
|
+
// =========================================================================
|
|
579
|
+
{
|
|
580
|
+
id: "discord-bot-token",
|
|
581
|
+
name: "Discord Bot Token",
|
|
582
|
+
pattern: /(?:^|["'\s:=,`])([MN][A-Za-z\d]{23,}\.[\w-]{6}\.[\w-]{27})(?:["'\s,`;]|$)/gm,
|
|
583
|
+
confidence: 0.9,
|
|
584
|
+
severity: "critical",
|
|
585
|
+
description: "Discord Bot Token. Grants full bot access to Discord guilds and channels.",
|
|
586
|
+
remediation: "Reset the token in the Discord Developer Portal > Bot page. Store in environment variables.",
|
|
587
|
+
category: "messaging"
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
id: "discord-client-secret",
|
|
591
|
+
name: "Discord Client Secret",
|
|
592
|
+
pattern: /(?:^|["'\s:=,`])([A-Za-z0-9_-]{32})(?:["'\s,`;]|$)/gm,
|
|
593
|
+
confidence: 0.5,
|
|
594
|
+
severity: "high",
|
|
595
|
+
description: "Potential Discord OAuth2 Client Secret.",
|
|
596
|
+
remediation: "Reset the client secret in the Discord Developer Portal. Store in a vault.",
|
|
597
|
+
category: "messaging"
|
|
598
|
+
},
|
|
599
|
+
// =========================================================================
|
|
600
|
+
// Mailgun
|
|
601
|
+
// =========================================================================
|
|
602
|
+
{
|
|
603
|
+
id: "mailgun-api-key",
|
|
604
|
+
name: "Mailgun API Key",
|
|
605
|
+
pattern: /(?:^|["'\s:=,`])(key-[A-Za-z0-9]{32})(?:["'\s,`;]|$)/gm,
|
|
606
|
+
confidence: 0.85,
|
|
607
|
+
severity: "critical",
|
|
608
|
+
description: "Mailgun API Key. Grants access to send emails via Mailgun.",
|
|
609
|
+
remediation: "Rotate the API key in the Mailgun Dashboard. Store in a secrets manager.",
|
|
610
|
+
category: "email"
|
|
611
|
+
},
|
|
612
|
+
// =========================================================================
|
|
613
|
+
// Base64-Encoded Credentials
|
|
614
|
+
// =========================================================================
|
|
615
|
+
{
|
|
616
|
+
id: "base64-credentials",
|
|
617
|
+
name: "Base64-Encoded Credentials",
|
|
618
|
+
pattern: /(?:^|["'\s:=,`])(Basic\s+[A-Za-z0-9+/]{20,}={0,2})(?:["'\s,`;]|$)/gm,
|
|
619
|
+
confidence: 0.7,
|
|
620
|
+
severity: "high",
|
|
621
|
+
description: "Base64-encoded HTTP Basic Auth credentials (Basic base64(user:pass)).",
|
|
622
|
+
remediation: "Use OAuth2 or token-based authentication. Store credentials in a vault.",
|
|
623
|
+
category: "auth"
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
id: "base64-encoded-secret",
|
|
627
|
+
name: "Base64-Encoded Secret String",
|
|
628
|
+
pattern: /(?:^|["'\s:=,`])(?:password|secret|token|credential|apikey)(?:["'\s]*)[:=](?:["'\s]*)([A-Za-z0-9+/]{40,}={0,2})(?:["'\s,`;]|$)/gim,
|
|
629
|
+
confidence: 0.65,
|
|
630
|
+
severity: "high",
|
|
631
|
+
description: "A base64-encoded value assigned to a secret-related variable name.",
|
|
632
|
+
remediation: "Decode to verify, then store in a secrets manager. Use proper secret management.",
|
|
633
|
+
category: "auth"
|
|
634
|
+
},
|
|
635
|
+
// =========================================================================
|
|
636
|
+
// Generic Variable Assignments
|
|
637
|
+
// =========================================================================
|
|
638
|
+
{
|
|
639
|
+
id: "generic-api-key-assignment",
|
|
640
|
+
name: "Generic API Key Assignment",
|
|
641
|
+
pattern: /(?:^|["'\s])(?:API[_-]?KEY|api[_-]?key)\s*[:=]\s*["']([^\s"'`]{8,})["']/gm,
|
|
642
|
+
confidence: 0.7,
|
|
643
|
+
severity: "high",
|
|
644
|
+
description: "Generic API_KEY variable assignment with a suspicious value.",
|
|
645
|
+
remediation: "Move API keys to a secrets manager or environment variables. Never hardcode in source.",
|
|
646
|
+
category: "generic"
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
id: "generic-token-assignment",
|
|
650
|
+
name: "Generic Token Assignment",
|
|
651
|
+
pattern: /(?:^|["'\s])(?:TOKEN|token|Token)\s*[:=]\s*["']([^\s"'`]{8,})["']/gm,
|
|
652
|
+
confidence: 0.6,
|
|
653
|
+
severity: "high",
|
|
654
|
+
description: "Generic TOKEN variable assignment with a suspicious value.",
|
|
655
|
+
remediation: "Move tokens to a secrets manager. Use secure token storage mechanisms.",
|
|
656
|
+
category: "generic"
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
id: "generic-secret-assignment",
|
|
660
|
+
name: "Generic Secret Assignment",
|
|
661
|
+
pattern: /(?:^|["'\s])(?:SECRET|secret|Secret)[_-]?(?:KEY|key|Key)?\s*[:=]\s*["']([^\s"'`]{8,})["']/gm,
|
|
662
|
+
confidence: 0.65,
|
|
663
|
+
severity: "high",
|
|
664
|
+
description: "Generic SECRET variable assignment with a suspicious value.",
|
|
665
|
+
remediation: "Move secrets to a vault. Use encrypted storage for sensitive configuration.",
|
|
666
|
+
category: "generic"
|
|
667
|
+
},
|
|
668
|
+
{
|
|
669
|
+
id: "generic-password-assignment",
|
|
670
|
+
name: "Generic Password Assignment",
|
|
671
|
+
pattern: /(?:^|["'\s])(?:PASSWORD|passwd|password|Password)\s*[:=]\s*["']([^\s"'`]{4,})["']/gm,
|
|
672
|
+
confidence: 0.75,
|
|
673
|
+
severity: "high",
|
|
674
|
+
description: "Generic PASSWORD variable assignment with a value.",
|
|
675
|
+
remediation: "Never hardcode passwords. Use a secrets manager or secure credential store.",
|
|
676
|
+
category: "generic"
|
|
677
|
+
},
|
|
678
|
+
// =========================================================================
|
|
679
|
+
// .env File Patterns
|
|
680
|
+
// =========================================================================
|
|
681
|
+
{
|
|
682
|
+
id: "env-file-secret-pattern",
|
|
683
|
+
name: ".env File Secret Pattern",
|
|
684
|
+
pattern: /(?:^|[\r\n])([A-Z][A-Z0-9_]*(?:(?:PASSWORD|SECRET|TOKEN|KEY|CREDENTIAL|PRIVATE|AUTH)[A-Z0-9_]*)\s*=\s*[^\s][^\r\n]{7,})(?:[\r\n]|$)/gm,
|
|
685
|
+
confidence: 0.7,
|
|
686
|
+
severity: "high",
|
|
687
|
+
description: "Environment variable with a secret-like name and a value in an .env file.",
|
|
688
|
+
remediation: "Use ultraenv vault encryption for sensitive .env values. Never commit .env files to source control.",
|
|
689
|
+
category: "env"
|
|
690
|
+
},
|
|
691
|
+
// =========================================================================
|
|
692
|
+
// Hardcoded Passwords
|
|
693
|
+
// =========================================================================
|
|
694
|
+
{
|
|
695
|
+
id: "hardcoded-password-url",
|
|
696
|
+
name: "Hardcoded Password in URL",
|
|
697
|
+
pattern: /(?:https?:\/\/)[^\s"'`]*:[^\s"'`]*@(?:[^\s"'`]+)/g,
|
|
698
|
+
confidence: 0.8,
|
|
699
|
+
severity: "critical",
|
|
700
|
+
description: "URL containing embedded credentials (protocol://user:password@host).",
|
|
701
|
+
remediation: "Remove credentials from URLs. Use environment variables for authentication parameters.",
|
|
702
|
+
category: "generic"
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
id: "hardcoded-password-string",
|
|
706
|
+
name: "Hardcoded Password String",
|
|
707
|
+
pattern: /(?:password|passwd|pwd)\s*[:=]\s*["']([^"']{4,}?)["']/gim,
|
|
708
|
+
confidence: 0.7,
|
|
709
|
+
severity: "high",
|
|
710
|
+
description: "Hardcoded password string in configuration or code.",
|
|
711
|
+
remediation: "Replace with references to a secrets manager. Never store plain-text passwords in code.",
|
|
712
|
+
category: "generic"
|
|
713
|
+
},
|
|
714
|
+
// =========================================================================
|
|
715
|
+
// SSH
|
|
716
|
+
// =========================================================================
|
|
717
|
+
{
|
|
718
|
+
id: "ssh-private-key",
|
|
719
|
+
name: "SSH Private Key",
|
|
720
|
+
pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/gm,
|
|
721
|
+
confidence: 0.99,
|
|
722
|
+
severity: "critical",
|
|
723
|
+
description: "SSH Private Key in any PEM format. Grants remote access to systems.",
|
|
724
|
+
remediation: "Remove from source immediately. Generate a new SSH key pair. Store private key in SSH agent or vault.",
|
|
725
|
+
category: "keys"
|
|
726
|
+
},
|
|
727
|
+
// =========================================================================
|
|
728
|
+
// Cloudflare
|
|
729
|
+
// =========================================================================
|
|
730
|
+
{
|
|
731
|
+
id: "cloudflare-api-token",
|
|
732
|
+
name: "Cloudflare API Token",
|
|
733
|
+
pattern: /(?:^|["'\s:=,`])(v1\.0-[A-Za-z0-9_-]{35,}-[A-Za-z0-9_-]{15,})(?:["'\s,`;]|$)/gm,
|
|
734
|
+
confidence: 0.9,
|
|
735
|
+
severity: "critical",
|
|
736
|
+
description: "Cloudflare API Token. Grants access to Cloudflare services and zones.",
|
|
737
|
+
remediation: "Revoke the token in Cloudflare Dashboard > My Profile > API Tokens. Store securely.",
|
|
738
|
+
category: "cloud"
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
id: "cloudflare-global-api-key",
|
|
742
|
+
name: "Cloudflare Global API Key",
|
|
743
|
+
pattern: /(?:^|["'\s:=,`])([A-Za-z0-9]{37})(?:["'\s,`;]|$)/gm,
|
|
744
|
+
confidence: 0.4,
|
|
745
|
+
severity: "high",
|
|
746
|
+
description: "Potential Cloudflare Global API Key (24 hex + 13 chars).",
|
|
747
|
+
remediation: "Revoke the key in Cloudflare Dashboard. Use scoped API tokens instead of global keys.",
|
|
748
|
+
category: "cloud"
|
|
749
|
+
},
|
|
750
|
+
// =========================================================================
|
|
751
|
+
// Auth0
|
|
752
|
+
// =========================================================================
|
|
753
|
+
{
|
|
754
|
+
id: "auth0-client-secret",
|
|
755
|
+
name: "Auth0 Client Secret",
|
|
756
|
+
pattern: /(?:^|["'\s:=,`])([A-Za-z0-9_-]{40,})(?:["'\s,`;]|$)(?:\s*#.*auth0.*)/gm,
|
|
757
|
+
confidence: 0.5,
|
|
758
|
+
severity: "high",
|
|
759
|
+
description: "Potential Auth0 Client Secret (long random string near Auth0 reference).",
|
|
760
|
+
remediation: "Rotate the client secret in Auth0 Dashboard > Applications. Store in a vault.",
|
|
761
|
+
category: "auth"
|
|
762
|
+
},
|
|
763
|
+
// =========================================================================
|
|
764
|
+
// PagerDuty
|
|
765
|
+
// =========================================================================
|
|
766
|
+
{
|
|
767
|
+
id: "pagerduty-token",
|
|
768
|
+
name: "PagerDuty API Token",
|
|
769
|
+
pattern: /(?:^|["'\s:=,`])(PD-[A-Za-z0-9]{20,})(?:["'\s,`;]|$)/gm,
|
|
770
|
+
confidence: 0.85,
|
|
771
|
+
severity: "high",
|
|
772
|
+
description: "PagerDuty API Token / Events API Key.",
|
|
773
|
+
remediation: "Rotate the token in PagerDuty. Store in environment variables.",
|
|
774
|
+
category: "devops"
|
|
775
|
+
},
|
|
776
|
+
// =========================================================================
|
|
777
|
+
// Datadog
|
|
778
|
+
// =========================================================================
|
|
779
|
+
{
|
|
780
|
+
id: "datadog-api-key",
|
|
781
|
+
name: "Datadog API Key",
|
|
782
|
+
pattern: /(?:^|["'\s:=,`])(dd_[A-Za-z0-9]{32,})(?:["'\s,`;]|$)/gm,
|
|
783
|
+
confidence: 0.85,
|
|
784
|
+
severity: "high",
|
|
785
|
+
description: "Datadog API Key. Grants access to monitoring and alerting APIs.",
|
|
786
|
+
remediation: "Rotate the API key in Datadog. Store in environment variables or a vault.",
|
|
787
|
+
category: "monitoring"
|
|
788
|
+
},
|
|
789
|
+
// =========================================================================
|
|
790
|
+
// Generic High-Entropy Strings
|
|
791
|
+
// =========================================================================
|
|
792
|
+
{
|
|
793
|
+
id: "generic-high-entropy-string",
|
|
794
|
+
name: "Generic High-Entropy String",
|
|
795
|
+
pattern: /(?:^|["'\s:=,`])([A-Za-z0-9+/=]{40,})(?:["'\s,`;]|$)/gm,
|
|
796
|
+
confidence: 0.3,
|
|
797
|
+
severity: "medium",
|
|
798
|
+
description: "Long base64-like string that may be a secret, token, or encoded credential.",
|
|
799
|
+
remediation: "Review the value to determine if it is sensitive. Move to a secrets manager if confirmed.",
|
|
800
|
+
category: "generic"
|
|
801
|
+
}
|
|
802
|
+
];
|
|
803
|
+
|
|
804
|
+
// src/scanner/patterns.ts
|
|
805
|
+
var patternRegistry = [...SECRET_PATTERNS];
|
|
806
|
+
function offsetToLine(content, offset) {
|
|
807
|
+
let line = 1;
|
|
808
|
+
for (let i = 0; i < offset && i < content.length; i++) {
|
|
809
|
+
if (content[i] === "\n") {
|
|
810
|
+
line++;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
return line;
|
|
814
|
+
}
|
|
815
|
+
function offsetToColumn(content, offset) {
|
|
816
|
+
let lastNewline = -1;
|
|
817
|
+
for (let i = offset - 1; i >= 0; i--) {
|
|
818
|
+
if (content[i] === "\n") {
|
|
819
|
+
lastNewline = i;
|
|
820
|
+
break;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return offset - lastNewline;
|
|
824
|
+
}
|
|
825
|
+
function extractVarName(line, matchedValue) {
|
|
826
|
+
const varMatch = /^(?:export\s+)?([A-Z][A-Z0-9_]*)\s*[=:]\s*/i.exec(line);
|
|
827
|
+
if (varMatch) {
|
|
828
|
+
const varName = varMatch[1];
|
|
829
|
+
const assignEnd = varMatch[0].length;
|
|
830
|
+
if (line.indexOf(matchedValue, assignEnd) !== -1) {
|
|
831
|
+
return varName;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return void 0;
|
|
835
|
+
}
|
|
836
|
+
function getLineAtOffset(content, offset) {
|
|
837
|
+
const start = content.lastIndexOf("\n", offset - 1) + 1;
|
|
838
|
+
const end = content.indexOf("\n", offset);
|
|
839
|
+
return content.slice(start, end === -1 ? void 0 : end);
|
|
840
|
+
}
|
|
841
|
+
function matchSinglePattern(content, pattern, filePath) {
|
|
842
|
+
const results = [];
|
|
843
|
+
const regex = new RegExp(pattern.pattern.source, pattern.pattern.flags);
|
|
844
|
+
regex.lastIndex = 0;
|
|
845
|
+
let match;
|
|
846
|
+
while ((match = regex.exec(content)) !== null) {
|
|
847
|
+
const fullMatch = match[0] ?? "";
|
|
848
|
+
const matchStart = match.index;
|
|
849
|
+
let secretValue = fullMatch;
|
|
850
|
+
for (let g = match.length - 1; g >= 1; g--) {
|
|
851
|
+
const groupValue = match[g];
|
|
852
|
+
if (groupValue !== void 0 && groupValue !== "") {
|
|
853
|
+
secretValue = groupValue;
|
|
854
|
+
const groupStart = fullMatch.indexOf(groupValue);
|
|
855
|
+
if (groupStart !== -1) {
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
const line = offsetToLine(content, matchStart);
|
|
861
|
+
const column = offsetToColumn(content, matchStart);
|
|
862
|
+
const lineText = getLineAtOffset(content, matchStart);
|
|
863
|
+
const varName = extractVarName(lineText, secretValue);
|
|
864
|
+
results.push({
|
|
865
|
+
type: pattern.name,
|
|
866
|
+
value: maskValue(secretValue),
|
|
867
|
+
file: filePath,
|
|
868
|
+
line,
|
|
869
|
+
column,
|
|
870
|
+
pattern,
|
|
871
|
+
confidence: pattern.confidence,
|
|
872
|
+
varName
|
|
873
|
+
});
|
|
874
|
+
if (fullMatch.length === 0) {
|
|
875
|
+
regex.lastIndex++;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
return results;
|
|
879
|
+
}
|
|
880
|
+
function matchPatterns(content, filePath) {
|
|
881
|
+
const allDetections = [];
|
|
882
|
+
for (const pattern of patternRegistry) {
|
|
883
|
+
const detections = matchSinglePattern(content, pattern, filePath);
|
|
884
|
+
allDetections.push(...detections);
|
|
885
|
+
}
|
|
886
|
+
return allDetections;
|
|
887
|
+
}
|
|
888
|
+
function addCustomPattern(pattern) {
|
|
889
|
+
if (!(pattern.pattern instanceof RegExp)) {
|
|
890
|
+
throw new Error(`Pattern "${pattern.id}" must have a valid RegExp instance.`);
|
|
891
|
+
}
|
|
892
|
+
try {
|
|
893
|
+
pattern.pattern.lastIndex = 0;
|
|
894
|
+
pattern.pattern.test("");
|
|
895
|
+
pattern.pattern.lastIndex = 0;
|
|
896
|
+
} catch (err) {
|
|
897
|
+
throw new Error(`Pattern "${pattern.id}" has an invalid regex: ${err.message}`);
|
|
898
|
+
}
|
|
899
|
+
const existingIndex = patternRegistry.findIndex((p) => p.id === pattern.id);
|
|
900
|
+
if (existingIndex !== -1) {
|
|
901
|
+
patternRegistry[existingIndex] = pattern;
|
|
902
|
+
} else {
|
|
903
|
+
patternRegistry.push(pattern);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
function removeCustomPattern(id) {
|
|
907
|
+
const index = patternRegistry.findIndex((p) => p.id === id);
|
|
908
|
+
if (index !== -1) {
|
|
909
|
+
patternRegistry.splice(index, 1);
|
|
910
|
+
return true;
|
|
911
|
+
}
|
|
912
|
+
return false;
|
|
913
|
+
}
|
|
914
|
+
function resetPatterns() {
|
|
915
|
+
patternRegistry.length = 0;
|
|
916
|
+
patternRegistry.push(...SECRET_PATTERNS);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// src/utils/entropy.ts
|
|
920
|
+
function shannonEntropy(str) {
|
|
921
|
+
if (str.length === 0) return 0;
|
|
922
|
+
const freq = /* @__PURE__ */ new Map();
|
|
923
|
+
for (const char of str) {
|
|
924
|
+
freq.set(char, (freq.get(char) ?? 0) + 1);
|
|
925
|
+
}
|
|
926
|
+
const len = str.length;
|
|
927
|
+
let entropy = 0;
|
|
928
|
+
for (const count of freq.values()) {
|
|
929
|
+
const probability = count / len;
|
|
930
|
+
entropy -= probability * Math.log2(probability);
|
|
931
|
+
}
|
|
932
|
+
return entropy;
|
|
933
|
+
}
|
|
934
|
+
function isHighEntropy(str, threshold = 3.5) {
|
|
935
|
+
if (str.length === 0) return false;
|
|
936
|
+
return shannonEntropy(str) > threshold;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// src/scanner/entropy.ts
|
|
940
|
+
var DEFAULT_ENTROPY_OPTIONS = {
|
|
941
|
+
threshold: 3.5,
|
|
942
|
+
minLength: 20,
|
|
943
|
+
maxLength: 500,
|
|
944
|
+
includeDefaultPattern: true
|
|
945
|
+
};
|
|
946
|
+
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
947
|
+
var COMMIT_HASH_PATTERN = /^[0-9a-f]{7,40}$/;
|
|
948
|
+
var SEMVER_PATTERN = /^v?[0-9]+\.[0-9]+\.[0-9]+(?:-[a-zA-Z0-9.]+)?(?:\+[a-zA-Z0-9.]+)?$/;
|
|
949
|
+
var COMMON_WORDS = /* @__PURE__ */ new Set([
|
|
950
|
+
"the",
|
|
951
|
+
"quick",
|
|
952
|
+
"brown",
|
|
953
|
+
"jumps",
|
|
954
|
+
"over",
|
|
955
|
+
"lazy",
|
|
956
|
+
"undefined",
|
|
957
|
+
"null",
|
|
958
|
+
"boolean",
|
|
959
|
+
"string",
|
|
960
|
+
"number",
|
|
961
|
+
"object",
|
|
962
|
+
"function",
|
|
963
|
+
"prototype",
|
|
964
|
+
"constructor",
|
|
965
|
+
"return",
|
|
966
|
+
"typeof",
|
|
967
|
+
"instanceof"
|
|
968
|
+
]);
|
|
969
|
+
var PATH_PATTERN = /^(?:\/[a-zA-Z0-9_./-]+|[a-zA-Z]:\\[a-zA-Z0-9_.\\-]+)$/;
|
|
970
|
+
var URL_PATTERN = /^https?:\/\/[a-zA-Z0-9._/-]+(?:\?[a-zA-Z0-9._/&=%-]+)?(?:#[a-zA-Z0-9._-]+)?$/;
|
|
971
|
+
function isFalsePositive(candidate) {
|
|
972
|
+
if (candidate.length === 0) return true;
|
|
973
|
+
if (UUID_PATTERN.test(candidate)) return true;
|
|
974
|
+
if (COMMIT_HASH_PATTERN.test(candidate)) return true;
|
|
975
|
+
if (SEMVER_PATTERN.test(candidate)) return true;
|
|
976
|
+
if (COMMON_WORDS.has(candidate.toLowerCase())) return true;
|
|
977
|
+
if (PATH_PATTERN.test(candidate)) return true;
|
|
978
|
+
if (URL_PATTERN.test(candidate)) return true;
|
|
979
|
+
const nonAlphaNumeric = (candidate.match(/[^a-zA-Z0-9]/g) ?? []).length;
|
|
980
|
+
if (nonAlphaNumeric / candidate.length > 0.8) return true;
|
|
981
|
+
if (/^\d+$/.test(candidate)) return true;
|
|
982
|
+
if (/^(.)\1{10,}$/.test(candidate)) return true;
|
|
983
|
+
return false;
|
|
984
|
+
}
|
|
985
|
+
var ENTROPY_SECRET_PATTERN = {
|
|
986
|
+
id: "entropy-high-entropy-string",
|
|
987
|
+
name: "High-Entropy String",
|
|
988
|
+
pattern: /./,
|
|
989
|
+
// Placeholder; actual detection is entropy-based
|
|
990
|
+
confidence: 0.5,
|
|
991
|
+
severity: "medium",
|
|
992
|
+
description: "String with high Shannon entropy that may be a secret or token not caught by specific patterns.",
|
|
993
|
+
remediation: "Review this value to determine if it is sensitive. If confirmed as a secret, move to a vault or environment variable.",
|
|
994
|
+
category: "entropy"
|
|
995
|
+
};
|
|
996
|
+
function extractCandidates(line) {
|
|
997
|
+
const candidates = [];
|
|
998
|
+
const quotePatterns = [
|
|
999
|
+
{ regex: /"([^"]+)"/g, offset: 0 },
|
|
1000
|
+
{ regex: /'([^']+)'/g, offset: 0 },
|
|
1001
|
+
{ regex: /`([^`]+)`/g, offset: 0 }
|
|
1002
|
+
];
|
|
1003
|
+
for (const { regex } of quotePatterns) {
|
|
1004
|
+
let match;
|
|
1005
|
+
while ((match = regex.exec(line)) !== null) {
|
|
1006
|
+
const captured = match[1];
|
|
1007
|
+
if (captured !== void 0) {
|
|
1008
|
+
candidates.push({ value: captured, column: match.index + 1 });
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
const assignPattern = /(?:^|[;\s])([A-Z][A-Z0-9_]*)\s*[=:]\s*([^\s;'"`]+)/gi;
|
|
1013
|
+
let assignMatch;
|
|
1014
|
+
while ((assignMatch = assignPattern.exec(line)) !== null) {
|
|
1015
|
+
const value = assignMatch[2];
|
|
1016
|
+
if (value !== void 0 && value.length >= DEFAULT_ENTROPY_OPTIONS.minLength) {
|
|
1017
|
+
const valueStart = line.indexOf(value, assignMatch.index);
|
|
1018
|
+
candidates.push({ value, column: valueStart + 1 });
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
const tokenPattern = /([A-Za-z0-9+/=_-]{20,})/g;
|
|
1022
|
+
let tokenMatch;
|
|
1023
|
+
while ((tokenMatch = tokenPattern.exec(line)) !== null) {
|
|
1024
|
+
const captured = tokenMatch[1];
|
|
1025
|
+
if (captured === void 0) continue;
|
|
1026
|
+
const tokenIndex = tokenMatch.index + 1;
|
|
1027
|
+
const existing = candidates.some(
|
|
1028
|
+
(c) => c.value === captured && c.column === tokenIndex
|
|
1029
|
+
);
|
|
1030
|
+
if (!existing) {
|
|
1031
|
+
candidates.push({ value: captured, column: tokenMatch.index + 1 });
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
return candidates;
|
|
1035
|
+
}
|
|
1036
|
+
function scanLineForEntropy(line, lineNumber, filePath, options) {
|
|
1037
|
+
const opts = { ...DEFAULT_ENTROPY_OPTIONS, ...options };
|
|
1038
|
+
const detections = [];
|
|
1039
|
+
if (line.trim().length === 0) return detections;
|
|
1040
|
+
const candidates = extractCandidates(line);
|
|
1041
|
+
for (const candidate of candidates) {
|
|
1042
|
+
const { value, column } = candidate;
|
|
1043
|
+
if (value.length < opts.minLength || value.length > opts.maxLength) continue;
|
|
1044
|
+
if (isFalsePositive(value)) continue;
|
|
1045
|
+
const entropy = shannonEntropy(value);
|
|
1046
|
+
if (entropy >= opts.threshold) {
|
|
1047
|
+
const excessEntropy = entropy - opts.threshold;
|
|
1048
|
+
const confidence = Math.min(0.9, 0.4 + excessEntropy * 0.15);
|
|
1049
|
+
detections.push({
|
|
1050
|
+
type: ENTROPY_SECRET_PATTERN.name,
|
|
1051
|
+
value: maskValue(value),
|
|
1052
|
+
file: filePath,
|
|
1053
|
+
line: lineNumber,
|
|
1054
|
+
column,
|
|
1055
|
+
pattern: ENTROPY_SECRET_PATTERN,
|
|
1056
|
+
confidence
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
return detections;
|
|
1061
|
+
}
|
|
1062
|
+
function detectHighEntropyStrings(content, filePath, options) {
|
|
1063
|
+
const lines = content.split("\n");
|
|
1064
|
+
const allDetections = [];
|
|
1065
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1066
|
+
const lineNumber = i + 1;
|
|
1067
|
+
const line = lines[i];
|
|
1068
|
+
if (line === void 0) continue;
|
|
1069
|
+
const detections = scanLineForEntropy(line, lineNumber, filePath, options);
|
|
1070
|
+
allDetections.push(...detections);
|
|
1071
|
+
}
|
|
1072
|
+
return allDetections;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// src/scanner/file-scanner.ts
|
|
1076
|
+
import { promises as fsp } from "fs";
|
|
1077
|
+
import { join, resolve, relative } from "path";
|
|
1078
|
+
var DEFAULT_SKIP_PATHS = /* @__PURE__ */ new Set([
|
|
1079
|
+
"node_modules",
|
|
1080
|
+
".git",
|
|
1081
|
+
"dist",
|
|
1082
|
+
"build",
|
|
1083
|
+
".next",
|
|
1084
|
+
".nuxt",
|
|
1085
|
+
"coverage",
|
|
1086
|
+
".cache",
|
|
1087
|
+
".turbo",
|
|
1088
|
+
".vercel",
|
|
1089
|
+
".netlify",
|
|
1090
|
+
"__pycache__",
|
|
1091
|
+
".eggs",
|
|
1092
|
+
"*.egg-info",
|
|
1093
|
+
".tox",
|
|
1094
|
+
".mypy_cache",
|
|
1095
|
+
".pytest_cache",
|
|
1096
|
+
"vendor",
|
|
1097
|
+
".bundle"
|
|
1098
|
+
]);
|
|
1099
|
+
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1100
|
+
".png",
|
|
1101
|
+
".jpg",
|
|
1102
|
+
".jpeg",
|
|
1103
|
+
".gif",
|
|
1104
|
+
".bmp",
|
|
1105
|
+
".ico",
|
|
1106
|
+
".webp",
|
|
1107
|
+
".svg",
|
|
1108
|
+
".tiff",
|
|
1109
|
+
".mp3",
|
|
1110
|
+
".mp4",
|
|
1111
|
+
".avi",
|
|
1112
|
+
".mov",
|
|
1113
|
+
".wmv",
|
|
1114
|
+
".flv",
|
|
1115
|
+
".wav",
|
|
1116
|
+
".ogg",
|
|
1117
|
+
".m4a",
|
|
1118
|
+
".zip",
|
|
1119
|
+
".tar",
|
|
1120
|
+
".gz",
|
|
1121
|
+
".bz2",
|
|
1122
|
+
".7z",
|
|
1123
|
+
".rar",
|
|
1124
|
+
".xz",
|
|
1125
|
+
".zst",
|
|
1126
|
+
".pdf",
|
|
1127
|
+
".doc",
|
|
1128
|
+
".docx",
|
|
1129
|
+
".xls",
|
|
1130
|
+
".xlsx",
|
|
1131
|
+
".ppt",
|
|
1132
|
+
".pptx",
|
|
1133
|
+
".exe",
|
|
1134
|
+
".dll",
|
|
1135
|
+
".so",
|
|
1136
|
+
".dylib",
|
|
1137
|
+
".bin",
|
|
1138
|
+
".dat",
|
|
1139
|
+
".wasm",
|
|
1140
|
+
".ttf",
|
|
1141
|
+
".otf",
|
|
1142
|
+
".woff",
|
|
1143
|
+
".woff2",
|
|
1144
|
+
".eot",
|
|
1145
|
+
".sqlite",
|
|
1146
|
+
".sqlite3",
|
|
1147
|
+
".db",
|
|
1148
|
+
".class",
|
|
1149
|
+
".jar",
|
|
1150
|
+
".war",
|
|
1151
|
+
".ear",
|
|
1152
|
+
".pyc",
|
|
1153
|
+
".pyo",
|
|
1154
|
+
".o",
|
|
1155
|
+
".obj",
|
|
1156
|
+
".a",
|
|
1157
|
+
".lib"
|
|
1158
|
+
]);
|
|
1159
|
+
var BINARY_CHECK_SIZE = 8192;
|
|
1160
|
+
var DEFAULT_MAX_FILE_SIZE = 1024 * 1024;
|
|
1161
|
+
function getExtension(filePath) {
|
|
1162
|
+
const lastDot = filePath.lastIndexOf(".");
|
|
1163
|
+
if (lastDot === -1) return "";
|
|
1164
|
+
return filePath.slice(lastDot).toLowerCase();
|
|
1165
|
+
}
|
|
1166
|
+
function isBinaryExtension(filePath) {
|
|
1167
|
+
return BINARY_EXTENSIONS.has(getExtension(filePath));
|
|
1168
|
+
}
|
|
1169
|
+
function isInSkippedDirectory(filePath) {
|
|
1170
|
+
const parts = filePath.replace(/\\/g, "/").split("/");
|
|
1171
|
+
return parts.some((part) => DEFAULT_SKIP_PATHS.has(part));
|
|
1172
|
+
}
|
|
1173
|
+
function matchesExcludePattern(relPath, excludes) {
|
|
1174
|
+
for (const pattern of excludes) {
|
|
1175
|
+
const normalizedPattern = pattern.replace(/\\/g, "/").replace(/\./g, "\\.");
|
|
1176
|
+
if (normalizedPattern.includes("**")) {
|
|
1177
|
+
const regexStr = normalizedPattern.replace(/\*\*/g, "<<DOUBLESTAR>>").replace(/\*/g, "[^/]*").replace(/<<DOUBLESTAR>>/g, ".*");
|
|
1178
|
+
const regex = new RegExp(`^${regexStr}$`);
|
|
1179
|
+
if (regex.test(relPath)) return true;
|
|
1180
|
+
} else if (normalizedPattern.includes("*")) {
|
|
1181
|
+
const regexStr = normalizedPattern.replace(/\*/g, "[^/]*");
|
|
1182
|
+
const regex = new RegExp(`^${regexStr}$`);
|
|
1183
|
+
if (regex.test(relPath)) return true;
|
|
1184
|
+
} else {
|
|
1185
|
+
if (relPath === normalizedPattern || relPath.startsWith(normalizedPattern + "/")) {
|
|
1186
|
+
return true;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
return false;
|
|
1191
|
+
}
|
|
1192
|
+
function matchesIncludePattern(relPath, includes) {
|
|
1193
|
+
if (includes.length === 0) return true;
|
|
1194
|
+
for (const pattern of includes) {
|
|
1195
|
+
const normalizedPattern = pattern.replace(/\\/g, "/").replace(/\./g, "\\.");
|
|
1196
|
+
if (normalizedPattern.includes("**")) {
|
|
1197
|
+
const regexStr = normalizedPattern.replace(/\*\*/g, "<<DOUBLESTAR>>").replace(/\*/g, "[^/]*").replace(/<<DOUBLESTAR>>/g, ".*");
|
|
1198
|
+
const regex = new RegExp(`^${regexStr}$`);
|
|
1199
|
+
if (regex.test(relPath)) return true;
|
|
1200
|
+
} else if (normalizedPattern.includes("*")) {
|
|
1201
|
+
const regexStr = normalizedPattern.replace(/\*/g, "[^/]*");
|
|
1202
|
+
const regex = new RegExp(`^${regexStr}$`);
|
|
1203
|
+
if (regex.test(relPath)) return true;
|
|
1204
|
+
} else {
|
|
1205
|
+
if (relPath === normalizedPattern || relPath.startsWith(normalizedPattern + "/")) {
|
|
1206
|
+
return true;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
return false;
|
|
1211
|
+
}
|
|
1212
|
+
function isBinaryContent(buffer) {
|
|
1213
|
+
for (let i = 0; i < Math.min(buffer.length, BINARY_CHECK_SIZE); i++) {
|
|
1214
|
+
if (buffer[i] === 0) return true;
|
|
1215
|
+
}
|
|
1216
|
+
return false;
|
|
1217
|
+
}
|
|
1218
|
+
async function collectFiles(basePaths, options) {
|
|
1219
|
+
const files = [];
|
|
1220
|
+
const skipped = [];
|
|
1221
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1222
|
+
async function walk(dir) {
|
|
1223
|
+
const absDir = resolve(options.cwd, dir);
|
|
1224
|
+
const canonicalDir = resolve(absDir);
|
|
1225
|
+
if (visited.has(canonicalDir)) return;
|
|
1226
|
+
visited.add(canonicalDir);
|
|
1227
|
+
let entries;
|
|
1228
|
+
try {
|
|
1229
|
+
entries = await fsp.readdir(absDir, { withFileTypes: true });
|
|
1230
|
+
} catch {
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
for (const entry of entries) {
|
|
1234
|
+
const fullPath = join(absDir, entry.name);
|
|
1235
|
+
const relPath = relative(options.cwd, fullPath).replace(/\\/g, "/");
|
|
1236
|
+
if (matchesExcludePattern(relPath, options.excludes)) {
|
|
1237
|
+
skipped.push(relPath);
|
|
1238
|
+
continue;
|
|
1239
|
+
}
|
|
1240
|
+
if (entry.isDirectory() && DEFAULT_SKIP_PATHS.has(entry.name)) {
|
|
1241
|
+
skipped.push(relPath);
|
|
1242
|
+
continue;
|
|
1243
|
+
}
|
|
1244
|
+
if (entry.isDirectory()) {
|
|
1245
|
+
await walk(relPath);
|
|
1246
|
+
} else if (entry.isFile()) {
|
|
1247
|
+
if (!matchesIncludePattern(relPath, options.includes)) {
|
|
1248
|
+
skipped.push(relPath);
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1251
|
+
if (isBinaryExtension(relPath)) {
|
|
1252
|
+
skipped.push(relPath);
|
|
1253
|
+
continue;
|
|
1254
|
+
}
|
|
1255
|
+
files.push(relPath);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
for (const basePath of basePaths) {
|
|
1260
|
+
const absPath = resolve(options.cwd, basePath);
|
|
1261
|
+
try {
|
|
1262
|
+
const stat = await fsp.stat(absPath);
|
|
1263
|
+
if (stat.isFile()) {
|
|
1264
|
+
const relPath = relative(options.cwd, absPath).replace(/\\/g, "/");
|
|
1265
|
+
if (!matchesExcludePattern(relPath, options.excludes) && matchesIncludePattern(relPath, options.includes) && !isBinaryExtension(relPath)) {
|
|
1266
|
+
files.push(relPath);
|
|
1267
|
+
} else {
|
|
1268
|
+
skipped.push(relPath);
|
|
1269
|
+
}
|
|
1270
|
+
} else if (stat.isDirectory()) {
|
|
1271
|
+
await walk(basePath);
|
|
1272
|
+
}
|
|
1273
|
+
} catch {
|
|
1274
|
+
skipped.push(basePath);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
return { files, skipped };
|
|
1278
|
+
}
|
|
1279
|
+
async function scanFile(filePath, cwd) {
|
|
1280
|
+
const relPath = relative(cwd, filePath).replace(/\\/g, "/");
|
|
1281
|
+
if (isBinaryExtension(relPath)) return [];
|
|
1282
|
+
if (isInSkippedDirectory(relPath)) return [];
|
|
1283
|
+
let content;
|
|
1284
|
+
try {
|
|
1285
|
+
const buffer = await fsp.readFile(filePath);
|
|
1286
|
+
if (isBinaryContent(buffer)) return [];
|
|
1287
|
+
content = buffer.toString("utf-8");
|
|
1288
|
+
} catch {
|
|
1289
|
+
return [];
|
|
1290
|
+
}
|
|
1291
|
+
const patternDetections = matchPatterns(content, relPath);
|
|
1292
|
+
const entropyDetections = detectHighEntropyStrings(content, relPath);
|
|
1293
|
+
return [...patternDetections, ...entropyDetections];
|
|
1294
|
+
}
|
|
1295
|
+
async function scanFiles(paths, options) {
|
|
1296
|
+
const startTime = performance.now();
|
|
1297
|
+
const resolvedOptions = {
|
|
1298
|
+
include: options?.include ?? ["**"],
|
|
1299
|
+
exclude: options?.exclude ?? [],
|
|
1300
|
+
scanGitHistory: options?.scanGitHistory ?? false,
|
|
1301
|
+
maxFileSize: options?.maxFileSize ?? DEFAULT_MAX_FILE_SIZE,
|
|
1302
|
+
customPatterns: options?.customPatterns ?? [],
|
|
1303
|
+
includeDefaults: options?.includeDefaults ?? true,
|
|
1304
|
+
failFast: options?.failFast ?? false
|
|
1305
|
+
};
|
|
1306
|
+
const cwd = process.cwd();
|
|
1307
|
+
const { files, skipped: preSkipped } = await collectFiles(paths, {
|
|
1308
|
+
excludes: resolvedOptions.exclude,
|
|
1309
|
+
includes: resolvedOptions.include,
|
|
1310
|
+
maxFileSize: resolvedOptions.maxFileSize,
|
|
1311
|
+
cwd
|
|
1312
|
+
});
|
|
1313
|
+
const filesScanned = [];
|
|
1314
|
+
const filesSkipped = [...preSkipped];
|
|
1315
|
+
const allSecrets = [];
|
|
1316
|
+
for (const relPath of files) {
|
|
1317
|
+
const absPath = resolve(cwd, relPath);
|
|
1318
|
+
if (await isGitIgnored(relPath, cwd)) {
|
|
1319
|
+
filesSkipped.push(relPath);
|
|
1320
|
+
continue;
|
|
1321
|
+
}
|
|
1322
|
+
try {
|
|
1323
|
+
const stat = await fsp.stat(absPath);
|
|
1324
|
+
if (stat.size > resolvedOptions.maxFileSize) {
|
|
1325
|
+
filesSkipped.push(relPath);
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1328
|
+
} catch {
|
|
1329
|
+
filesSkipped.push(relPath);
|
|
1330
|
+
continue;
|
|
1331
|
+
}
|
|
1332
|
+
const secrets = await scanFile(absPath, cwd);
|
|
1333
|
+
filesScanned.push(relPath);
|
|
1334
|
+
allSecrets.push(...secrets);
|
|
1335
|
+
if (resolvedOptions.failFast && allSecrets.length > 0) {
|
|
1336
|
+
break;
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
const scanTimeMs = performance.now() - startTime;
|
|
1340
|
+
return {
|
|
1341
|
+
found: allSecrets.length > 0,
|
|
1342
|
+
secrets: allSecrets,
|
|
1343
|
+
filesScanned,
|
|
1344
|
+
filesSkipped,
|
|
1345
|
+
scanTimeMs,
|
|
1346
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// src/scanner/git-scanner.ts
|
|
1351
|
+
import { execFile } from "child_process";
|
|
1352
|
+
import { promisify } from "util";
|
|
1353
|
+
var execFileAsync = promisify(execFile);
|
|
1354
|
+
async function gitExec(args, cwd) {
|
|
1355
|
+
const { stdout } = await execFileAsync("git", [...args], {
|
|
1356
|
+
cwd: cwd ?? process.cwd(),
|
|
1357
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
1358
|
+
// 50MB for history scanning
|
|
1359
|
+
encoding: "utf-8",
|
|
1360
|
+
timeout: 6e4
|
|
1361
|
+
});
|
|
1362
|
+
return stdout;
|
|
1363
|
+
}
|
|
1364
|
+
function parseCommitBlocks(rawOutput) {
|
|
1365
|
+
const blocks = [];
|
|
1366
|
+
const lines = rawOutput.split("\n");
|
|
1367
|
+
let currentHash = "";
|
|
1368
|
+
let currentContent = [];
|
|
1369
|
+
for (const line of lines) {
|
|
1370
|
+
if (line.startsWith("commit ")) {
|
|
1371
|
+
if (currentHash !== "") {
|
|
1372
|
+
blocks.push({ hash: currentHash, content: currentContent.join("\n") });
|
|
1373
|
+
}
|
|
1374
|
+
currentHash = line.slice(7).trim().split(/\s+/)[0] ?? "";
|
|
1375
|
+
currentContent = [line];
|
|
1376
|
+
} else {
|
|
1377
|
+
currentContent.push(line);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
if (currentHash !== "") {
|
|
1381
|
+
blocks.push({ hash: currentHash, content: currentContent.join("\n") });
|
|
1382
|
+
}
|
|
1383
|
+
return blocks;
|
|
1384
|
+
}
|
|
1385
|
+
function extractDiffFiles(patchContent) {
|
|
1386
|
+
const files = /* @__PURE__ */ new Set();
|
|
1387
|
+
const diffFilePattern = /^diff --git a\/(.+?) b\/(.+?)$/gm;
|
|
1388
|
+
let match;
|
|
1389
|
+
while ((match = diffFilePattern.exec(patchContent)) !== null) {
|
|
1390
|
+
files.add(match[2] ?? match[1] ?? "");
|
|
1391
|
+
}
|
|
1392
|
+
return files;
|
|
1393
|
+
}
|
|
1394
|
+
function scanContentBlock(content, filePath, commitHash) {
|
|
1395
|
+
const patternDetections = matchPatterns(content, filePath);
|
|
1396
|
+
const entropyDetections = detectHighEntropyStrings(content, filePath);
|
|
1397
|
+
const detections = [...patternDetections, ...entropyDetections];
|
|
1398
|
+
if (commitHash !== void 0) {
|
|
1399
|
+
return detections.map((d) => ({
|
|
1400
|
+
...d,
|
|
1401
|
+
file: `${d.file} (commit: ${commitHash.slice(0, 8)})`
|
|
1402
|
+
}));
|
|
1403
|
+
}
|
|
1404
|
+
return detections;
|
|
1405
|
+
}
|
|
1406
|
+
async function scanStagedFiles(cwd) {
|
|
1407
|
+
const workingDir = cwd ?? process.cwd();
|
|
1408
|
+
if (!await isGitRepository(workingDir)) {
|
|
1409
|
+
return [];
|
|
1410
|
+
}
|
|
1411
|
+
const stagedFilesList = await getStagedFiles(workingDir);
|
|
1412
|
+
if (stagedFilesList.length === 0) {
|
|
1413
|
+
return [];
|
|
1414
|
+
}
|
|
1415
|
+
const allSecrets = [];
|
|
1416
|
+
for (const filePath of stagedFilesList) {
|
|
1417
|
+
if (await isGitIgnored(filePath, workingDir)) continue;
|
|
1418
|
+
try {
|
|
1419
|
+
const stagedContent = await gitExec(["show", `:${filePath}`], workingDir);
|
|
1420
|
+
const secrets = scanContentBlock(stagedContent, filePath);
|
|
1421
|
+
allSecrets.push(...secrets);
|
|
1422
|
+
} catch {
|
|
1423
|
+
continue;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
return allSecrets;
|
|
1427
|
+
}
|
|
1428
|
+
async function scanDiff(from, to, cwd) {
|
|
1429
|
+
const workingDir = cwd ?? process.cwd();
|
|
1430
|
+
if (!await isGitRepository(workingDir)) {
|
|
1431
|
+
return [];
|
|
1432
|
+
}
|
|
1433
|
+
const args = ["diff", from];
|
|
1434
|
+
if (to !== void 0) {
|
|
1435
|
+
args.push(to);
|
|
1436
|
+
}
|
|
1437
|
+
let diffContent;
|
|
1438
|
+
try {
|
|
1439
|
+
diffContent = await gitExec(args, workingDir);
|
|
1440
|
+
} catch {
|
|
1441
|
+
return [];
|
|
1442
|
+
}
|
|
1443
|
+
if (diffContent.trim() === "") {
|
|
1444
|
+
return [];
|
|
1445
|
+
}
|
|
1446
|
+
const allSecrets = [];
|
|
1447
|
+
const diffFiles = extractDiffFiles(diffContent);
|
|
1448
|
+
for (const filePath of diffFiles) {
|
|
1449
|
+
const fileDiffRegex = new RegExp(
|
|
1450
|
+
`diff --git a/${escapeRegex(filePath)} b/${escapeRegex(filePath)}[\\s\\S]*?(?=diff --git |$)`,
|
|
1451
|
+
"g"
|
|
1452
|
+
);
|
|
1453
|
+
let fileMatch;
|
|
1454
|
+
while ((fileMatch = fileDiffRegex.exec(diffContent)) !== null) {
|
|
1455
|
+
const patchContent = fileMatch[0];
|
|
1456
|
+
const secrets = scanContentBlock(patchContent, filePath, from);
|
|
1457
|
+
allSecrets.push(...secrets);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
return allSecrets;
|
|
1461
|
+
}
|
|
1462
|
+
async function scanGitHistory(options) {
|
|
1463
|
+
const startTime = performance.now();
|
|
1464
|
+
const workingDir = options?.cwd ?? process.cwd();
|
|
1465
|
+
if (!await isGitRepository(workingDir)) {
|
|
1466
|
+
return {
|
|
1467
|
+
found: false,
|
|
1468
|
+
secrets: [],
|
|
1469
|
+
filesScanned: [],
|
|
1470
|
+
filesSkipped: [],
|
|
1471
|
+
scanTimeMs: performance.now() - startTime,
|
|
1472
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
const toRef = options?.to ?? "HEAD";
|
|
1476
|
+
const depth = options?.allHistory ? void 0 : options?.depth ?? 100;
|
|
1477
|
+
const fromRef = options?.from ?? (depth !== void 0 ? `HEAD~${depth}` : void 0);
|
|
1478
|
+
const logArgs = ["log", "-p", "--format=commit %H"];
|
|
1479
|
+
if (fromRef !== void 0) {
|
|
1480
|
+
logArgs.push(`${fromRef}..${toRef}`);
|
|
1481
|
+
} else if (depth !== void 0) {
|
|
1482
|
+
logArgs.push(`-${depth}`, toRef);
|
|
1483
|
+
} else {
|
|
1484
|
+
logArgs.push("--all");
|
|
1485
|
+
}
|
|
1486
|
+
let logOutput;
|
|
1487
|
+
try {
|
|
1488
|
+
logOutput = await gitExec(logArgs, workingDir);
|
|
1489
|
+
} catch {
|
|
1490
|
+
return {
|
|
1491
|
+
found: false,
|
|
1492
|
+
secrets: [],
|
|
1493
|
+
filesScanned: [],
|
|
1494
|
+
filesSkipped: [],
|
|
1495
|
+
scanTimeMs: performance.now() - startTime,
|
|
1496
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
if (logOutput.trim() === "") {
|
|
1500
|
+
return {
|
|
1501
|
+
found: false,
|
|
1502
|
+
secrets: [],
|
|
1503
|
+
filesScanned: [],
|
|
1504
|
+
filesSkipped: [],
|
|
1505
|
+
scanTimeMs: performance.now() - startTime,
|
|
1506
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
const commitBlocks = parseCommitBlocks(logOutput);
|
|
1510
|
+
const allSecrets = [];
|
|
1511
|
+
const filesScanned = /* @__PURE__ */ new Set();
|
|
1512
|
+
for (const block of commitBlocks) {
|
|
1513
|
+
if (block.content.trim() === "") continue;
|
|
1514
|
+
const commitFiles = extractDiffFiles(block.content);
|
|
1515
|
+
for (const filePath of commitFiles) {
|
|
1516
|
+
filesScanned.add(filePath);
|
|
1517
|
+
const fileDiffRegex = new RegExp(
|
|
1518
|
+
`diff --git a/${escapeRegex(filePath)} b/${escapeRegex(filePath)}[\\s\\S]*?(?=diff --git |$)`,
|
|
1519
|
+
"g"
|
|
1520
|
+
);
|
|
1521
|
+
let fileMatch;
|
|
1522
|
+
while ((fileMatch = fileDiffRegex.exec(block.content)) !== null) {
|
|
1523
|
+
const patchContent = fileMatch[0];
|
|
1524
|
+
const secrets = scanContentBlock(patchContent, filePath, block.hash);
|
|
1525
|
+
allSecrets.push(...secrets);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
return {
|
|
1530
|
+
found: allSecrets.length > 0,
|
|
1531
|
+
secrets: allSecrets,
|
|
1532
|
+
filesScanned: [...filesScanned],
|
|
1533
|
+
filesSkipped: [],
|
|
1534
|
+
scanTimeMs: performance.now() - startTime,
|
|
1535
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
function escapeRegex(str) {
|
|
1539
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
// src/scanner/reporter.ts
|
|
1543
|
+
var SEVERITY_CONFIG = {
|
|
1544
|
+
critical: {
|
|
1545
|
+
icon: "\u274C",
|
|
1546
|
+
// ❌
|
|
1547
|
+
color: "#dc2626",
|
|
1548
|
+
terminalColor: "\x1B[31m",
|
|
1549
|
+
// red
|
|
1550
|
+
label: "CRITICAL",
|
|
1551
|
+
sarifLevel: "error"
|
|
1552
|
+
},
|
|
1553
|
+
high: {
|
|
1554
|
+
icon: "\u26A0\uFE0F",
|
|
1555
|
+
// ⚠️
|
|
1556
|
+
color: "#ea580c",
|
|
1557
|
+
terminalColor: "\x1B[33m",
|
|
1558
|
+
// yellow
|
|
1559
|
+
label: "HIGH",
|
|
1560
|
+
sarifLevel: "error"
|
|
1561
|
+
},
|
|
1562
|
+
medium: {
|
|
1563
|
+
icon: "\u2139\uFE0F",
|
|
1564
|
+
// ℹ️
|
|
1565
|
+
color: "#2563eb",
|
|
1566
|
+
terminalColor: "\x1B[36m",
|
|
1567
|
+
// cyan
|
|
1568
|
+
label: "MEDIUM",
|
|
1569
|
+
sarifLevel: "warning"
|
|
1570
|
+
},
|
|
1571
|
+
low: {
|
|
1572
|
+
icon: "\u2022",
|
|
1573
|
+
// •
|
|
1574
|
+
color: "#6b7280",
|
|
1575
|
+
terminalColor: "\x1B[90m",
|
|
1576
|
+
// gray
|
|
1577
|
+
label: "LOW",
|
|
1578
|
+
sarifLevel: "note"
|
|
1579
|
+
}
|
|
1580
|
+
};
|
|
1581
|
+
function getSeverity(pattern) {
|
|
1582
|
+
const severity = pattern.severity;
|
|
1583
|
+
if (severity !== void 0 && severity in SEVERITY_CONFIG) {
|
|
1584
|
+
return severity;
|
|
1585
|
+
}
|
|
1586
|
+
if (pattern.confidence >= 0.9) return "critical";
|
|
1587
|
+
if (pattern.confidence >= 0.7) return "high";
|
|
1588
|
+
return "medium";
|
|
1589
|
+
}
|
|
1590
|
+
var RESET = "\x1B[0m";
|
|
1591
|
+
var BOLD = "\x1B[1m";
|
|
1592
|
+
var DIM = "\x1B[2m";
|
|
1593
|
+
function formatTerminal(result) {
|
|
1594
|
+
const lines = [];
|
|
1595
|
+
const totalCount = result.secrets.length;
|
|
1596
|
+
const criticalCount = result.secrets.filter(
|
|
1597
|
+
(s) => getSeverity(s.pattern) === "critical"
|
|
1598
|
+
).length;
|
|
1599
|
+
const highCount = result.secrets.filter(
|
|
1600
|
+
(s) => getSeverity(s.pattern) === "high"
|
|
1601
|
+
).length;
|
|
1602
|
+
const mediumCount = result.secrets.filter(
|
|
1603
|
+
(s) => getSeverity(s.pattern) === "medium"
|
|
1604
|
+
).length;
|
|
1605
|
+
lines.push("");
|
|
1606
|
+
lines.push(`${BOLD}=== ultraenv Secret Scan Report ===${RESET}`);
|
|
1607
|
+
lines.push("");
|
|
1608
|
+
lines.push(` Files scanned: ${result.filesScanned.length}`);
|
|
1609
|
+
lines.push(` Files skipped: ${result.filesSkipped.length}`);
|
|
1610
|
+
lines.push(` Scan time: ${result.scanTimeMs.toFixed(0)}ms`);
|
|
1611
|
+
lines.push(` Timestamp: ${result.timestamp}`);
|
|
1612
|
+
lines.push("");
|
|
1613
|
+
if (totalCount === 0) {
|
|
1614
|
+
lines.push(`${BOLD}\u2705 No secrets detected.${RESET}`);
|
|
1615
|
+
lines.push("");
|
|
1616
|
+
return lines.join("\n");
|
|
1617
|
+
}
|
|
1618
|
+
const summaryParts = [];
|
|
1619
|
+
if (criticalCount > 0) {
|
|
1620
|
+
summaryParts.push(`${SEVERITY_CONFIG.critical.terminalColor}${BOLD}${criticalCount} critical${RESET}`);
|
|
1621
|
+
}
|
|
1622
|
+
if (highCount > 0) {
|
|
1623
|
+
summaryParts.push(`${SEVERITY_CONFIG.high.terminalColor}${BOLD}${highCount} high${RESET}`);
|
|
1624
|
+
}
|
|
1625
|
+
if (mediumCount > 0) {
|
|
1626
|
+
summaryParts.push(`${SEVERITY_CONFIG.medium.terminalColor}${mediumCount} medium${RESET}`);
|
|
1627
|
+
}
|
|
1628
|
+
lines.push(`${BOLD}Found ${totalCount} secret(s):${RESET} ${summaryParts.join(", ")}`);
|
|
1629
|
+
lines.push("");
|
|
1630
|
+
const colSev = " SEV ";
|
|
1631
|
+
const colFile = " FILE ";
|
|
1632
|
+
const colLine = "LINE";
|
|
1633
|
+
const colType = " TYPE ";
|
|
1634
|
+
const colValue = " VALUE ";
|
|
1635
|
+
lines.push(
|
|
1636
|
+
` ${DIM}${colSev} ${colFile.padEnd(35)} ${colLine.padStart(4)} ${colType.padEnd(28)} ${colValue}${RESET}`
|
|
1637
|
+
);
|
|
1638
|
+
lines.push(` ${DIM}${"\u2500".repeat(95)}${RESET}`);
|
|
1639
|
+
for (const secret of result.secrets) {
|
|
1640
|
+
const severity = getSeverity(secret.pattern);
|
|
1641
|
+
const config = SEVERITY_CONFIG[severity];
|
|
1642
|
+
const sevStr = `${config.terminalColor}${config.label.padEnd(8)}${RESET}`;
|
|
1643
|
+
const fileStr = secret.file.length > 34 ? "..." + secret.file.slice(-31) : secret.file.padEnd(35);
|
|
1644
|
+
const lineStr = String(secret.line).padStart(4);
|
|
1645
|
+
const typeStr = secret.type.length > 27 ? secret.type.slice(0, 25) + ".." : secret.type.padEnd(28);
|
|
1646
|
+
const valueStr = secret.value.length > 25 ? secret.value.slice(0, 23) + ".." : secret.value;
|
|
1647
|
+
lines.push(` ${sevStr} ${DIM}${fileStr}${RESET} ${lineStr} ${typeStr} ${valueStr}`);
|
|
1648
|
+
}
|
|
1649
|
+
lines.push("");
|
|
1650
|
+
lines.push(`${BOLD}Remediation:${RESET}`);
|
|
1651
|
+
const seenRemediations = /* @__PURE__ */ new Set();
|
|
1652
|
+
let remediationCount = 0;
|
|
1653
|
+
for (const secret of result.secrets) {
|
|
1654
|
+
if (seenRemediations.has(secret.pattern.remediation)) continue;
|
|
1655
|
+
if (remediationCount >= 5) {
|
|
1656
|
+
const remaining = result.secrets.length - remediationCount;
|
|
1657
|
+
lines.push(` ${DIM}... and ${remaining} more finding(s)${RESET}`);
|
|
1658
|
+
break;
|
|
1659
|
+
}
|
|
1660
|
+
seenRemediations.add(secret.pattern.remediation);
|
|
1661
|
+
lines.push(` ${DIM}\u2022 ${secret.pattern.remediation}${RESET}`);
|
|
1662
|
+
remediationCount++;
|
|
1663
|
+
}
|
|
1664
|
+
lines.push("");
|
|
1665
|
+
lines.push(`${BOLD}Scan again with: ultraenv scan${RESET}`);
|
|
1666
|
+
lines.push("");
|
|
1667
|
+
return lines.join("\n");
|
|
1668
|
+
}
|
|
1669
|
+
function formatJson(result) {
|
|
1670
|
+
const output = {
|
|
1671
|
+
found: result.found,
|
|
1672
|
+
summary: {
|
|
1673
|
+
total: result.secrets.length,
|
|
1674
|
+
critical: result.secrets.filter((s) => getSeverity(s.pattern) === "critical").length,
|
|
1675
|
+
high: result.secrets.filter((s) => getSeverity(s.pattern) === "high").length,
|
|
1676
|
+
medium: result.secrets.filter((s) => getSeverity(s.pattern) === "medium").length
|
|
1677
|
+
},
|
|
1678
|
+
filesScanned: result.filesScanned,
|
|
1679
|
+
filesSkipped: result.filesSkipped,
|
|
1680
|
+
scanTimeMs: Math.round(result.scanTimeMs),
|
|
1681
|
+
timestamp: result.timestamp,
|
|
1682
|
+
secrets: result.secrets.map((secret) => ({
|
|
1683
|
+
type: secret.type,
|
|
1684
|
+
value: secret.value,
|
|
1685
|
+
file: secret.file,
|
|
1686
|
+
line: secret.line,
|
|
1687
|
+
column: secret.column,
|
|
1688
|
+
severity: getSeverity(secret.pattern),
|
|
1689
|
+
confidence: Math.round(secret.confidence * 100) / 100,
|
|
1690
|
+
patternId: secret.pattern.id,
|
|
1691
|
+
patternName: secret.pattern.name,
|
|
1692
|
+
category: secret.pattern.category ?? "unknown",
|
|
1693
|
+
description: secret.pattern.description,
|
|
1694
|
+
remediation: secret.pattern.remediation,
|
|
1695
|
+
varName: secret.varName
|
|
1696
|
+
}))
|
|
1697
|
+
};
|
|
1698
|
+
return JSON.stringify(output, null, 2);
|
|
1699
|
+
}
|
|
1700
|
+
function formatSarif(result) {
|
|
1701
|
+
const ruleMap = /* @__PURE__ */ new Map();
|
|
1702
|
+
for (const secret of result.secrets) {
|
|
1703
|
+
if (!ruleMap.has(secret.pattern.id)) {
|
|
1704
|
+
const severity = getSeverity(secret.pattern);
|
|
1705
|
+
const severityScore = severity === "critical" ? "9.0" : severity === "high" ? "7.0" : "4.0";
|
|
1706
|
+
const category = secret.pattern.category ?? "unknown";
|
|
1707
|
+
ruleMap.set(secret.pattern.id, {
|
|
1708
|
+
id: secret.pattern.id,
|
|
1709
|
+
name: secret.pattern.name,
|
|
1710
|
+
shortDescription: { text: secret.pattern.description },
|
|
1711
|
+
fullDescription: { text: `${secret.pattern.description}
|
|
1712
|
+
|
|
1713
|
+
${secret.pattern.remediation}` },
|
|
1714
|
+
helpUri: "https://docs.github.com/en/code-security/secret-scanning",
|
|
1715
|
+
properties: {
|
|
1716
|
+
"security-severity": severityScore,
|
|
1717
|
+
tags: ["security", "secret", category]
|
|
1718
|
+
},
|
|
1719
|
+
defaultConfiguration: {
|
|
1720
|
+
level: SEVERITY_CONFIG[severity].sarifLevel
|
|
1721
|
+
}
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
const sarifResults = result.secrets.map((secret) => ({
|
|
1726
|
+
ruleId: secret.pattern.id,
|
|
1727
|
+
level: SEVERITY_CONFIG[getSeverity(secret.pattern)].sarifLevel,
|
|
1728
|
+
message: {
|
|
1729
|
+
text: `Potential ${secret.type} detected (confidence: ${Math.round(secret.confidence * 100)}%). ${secret.pattern.description}`
|
|
1730
|
+
},
|
|
1731
|
+
locations: [
|
|
1732
|
+
{
|
|
1733
|
+
physicalLocation: {
|
|
1734
|
+
artifactLocation: {
|
|
1735
|
+
uri: secret.file
|
|
1736
|
+
},
|
|
1737
|
+
region: {
|
|
1738
|
+
startLine: secret.line,
|
|
1739
|
+
startColumn: secret.column
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
],
|
|
1744
|
+
properties: {
|
|
1745
|
+
confidence: Math.round(secret.confidence * 100) / 100,
|
|
1746
|
+
category: secret.pattern.category ?? "unknown"
|
|
1747
|
+
}
|
|
1748
|
+
}));
|
|
1749
|
+
const sarifDocument = {
|
|
1750
|
+
$schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
|
|
1751
|
+
version: "2.1.0",
|
|
1752
|
+
runs: [
|
|
1753
|
+
{
|
|
1754
|
+
tool: {
|
|
1755
|
+
driver: {
|
|
1756
|
+
name: "ultraenv",
|
|
1757
|
+
version: "1.0.0",
|
|
1758
|
+
informationUri: "https://github.com/Avinashvelu03/ultraenv",
|
|
1759
|
+
rules: [...ruleMap.values()]
|
|
1760
|
+
}
|
|
1761
|
+
},
|
|
1762
|
+
results: sarifResults
|
|
1763
|
+
}
|
|
1764
|
+
]
|
|
1765
|
+
};
|
|
1766
|
+
return JSON.stringify(sarifDocument, null, 2);
|
|
1767
|
+
}
|
|
1768
|
+
function formatScanResult(result, format) {
|
|
1769
|
+
switch (format) {
|
|
1770
|
+
case "terminal":
|
|
1771
|
+
return formatTerminal(result);
|
|
1772
|
+
case "json":
|
|
1773
|
+
return formatJson(result);
|
|
1774
|
+
case "sarif":
|
|
1775
|
+
return formatSarif(result);
|
|
1776
|
+
default: {
|
|
1777
|
+
const _exhaustive = format;
|
|
1778
|
+
throw new Error(`Unknown format: ${String(_exhaustive)}`);
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
// src/scanner/index.ts
|
|
1784
|
+
var CONFIG_FILENAME = ".ultraenvrc.json";
|
|
1785
|
+
async function loadConfig(cwd) {
|
|
1786
|
+
const { join: join2 } = await import("path");
|
|
1787
|
+
const { existsSync, readFileSync } = await import("fs");
|
|
1788
|
+
const configPath = join2(cwd, CONFIG_FILENAME);
|
|
1789
|
+
if (!existsSync(configPath)) return {};
|
|
1790
|
+
try {
|
|
1791
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
1792
|
+
const parsed = JSON.parse(raw);
|
|
1793
|
+
return parsed.scan ?? {};
|
|
1794
|
+
} catch {
|
|
1795
|
+
return {};
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
function dedupeKey(secret) {
|
|
1799
|
+
return `${secret.type}|${secret.file}|${secret.line}|${secret.column}`;
|
|
1800
|
+
}
|
|
1801
|
+
function deduplicateSecrets(secrets) {
|
|
1802
|
+
const map = /* @__PURE__ */ new Map();
|
|
1803
|
+
for (const secret of secrets) {
|
|
1804
|
+
const key = dedupeKey(secret);
|
|
1805
|
+
const existing = map.get(key);
|
|
1806
|
+
if (existing === void 0 || secret.confidence > existing.confidence) {
|
|
1807
|
+
map.set(key, secret);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
return [...map.values()];
|
|
1811
|
+
}
|
|
1812
|
+
var SEVERITY_ORDER = {
|
|
1813
|
+
critical: 0,
|
|
1814
|
+
high: 1,
|
|
1815
|
+
medium: 2,
|
|
1816
|
+
low: 3
|
|
1817
|
+
};
|
|
1818
|
+
function getSeverityFromPattern(pattern) {
|
|
1819
|
+
const patternWithSeverity = pattern;
|
|
1820
|
+
if (patternWithSeverity.severity !== void 0 && patternWithSeverity.severity in SEVERITY_ORDER) {
|
|
1821
|
+
return patternWithSeverity.severity;
|
|
1822
|
+
}
|
|
1823
|
+
if (pattern.confidence >= 0.9) return "critical";
|
|
1824
|
+
if (pattern.confidence >= 0.7) return "high";
|
|
1825
|
+
return "medium";
|
|
1826
|
+
}
|
|
1827
|
+
function sortBySeverity(secrets) {
|
|
1828
|
+
return [...secrets].sort((a, b) => {
|
|
1829
|
+
const sevA = SEVERITY_ORDER[getSeverityFromPattern(a.pattern)];
|
|
1830
|
+
const sevB = SEVERITY_ORDER[getSeverityFromPattern(b.pattern)];
|
|
1831
|
+
if (sevA !== sevB) return sevA - sevB;
|
|
1832
|
+
return b.confidence - a.confidence;
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1835
|
+
async function scan(options) {
|
|
1836
|
+
const startTime = performance.now();
|
|
1837
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
1838
|
+
const paths = options?.paths ?? ["."];
|
|
1839
|
+
const fileConfig = await loadConfig(cwd);
|
|
1840
|
+
const mergedOptions = {
|
|
1841
|
+
include: options?.include ?? fileConfig.include ?? ["**"],
|
|
1842
|
+
exclude: options?.exclude ?? fileConfig.exclude ?? [],
|
|
1843
|
+
scanGitHistory: options?.scanGitHistory ?? fileConfig.scanGitHistory ?? false,
|
|
1844
|
+
maxFileSize: options?.maxFileSize ?? fileConfig.maxFileSize ?? 1024 * 1024,
|
|
1845
|
+
customPatterns: options?.customPatterns ?? [],
|
|
1846
|
+
includeDefaults: options?.includeDefaults ?? true,
|
|
1847
|
+
failFast: options?.failFast ?? false
|
|
1848
|
+
};
|
|
1849
|
+
for (const customPattern of mergedOptions.customPatterns) {
|
|
1850
|
+
addCustomPattern(customPattern);
|
|
1851
|
+
}
|
|
1852
|
+
const allSecrets = [];
|
|
1853
|
+
const allFilesScanned = [];
|
|
1854
|
+
const allFilesSkipped = [];
|
|
1855
|
+
const fileResult = await scanFiles(paths, mergedOptions);
|
|
1856
|
+
allSecrets.push(...fileResult.secrets);
|
|
1857
|
+
allFilesScanned.push(...fileResult.filesScanned);
|
|
1858
|
+
allFilesSkipped.push(...fileResult.filesSkipped);
|
|
1859
|
+
if (mergedOptions.scanGitHistory) {
|
|
1860
|
+
const { isGitRepository: isGitRepository2 } = await import("./git-BZS4DPAI.js");
|
|
1861
|
+
if (await isGitRepository2(cwd)) {
|
|
1862
|
+
const gitResult = await scanGitHistory({
|
|
1863
|
+
depth: options?.gitDepth,
|
|
1864
|
+
allHistory: options?.gitAllHistory,
|
|
1865
|
+
cwd
|
|
1866
|
+
});
|
|
1867
|
+
allSecrets.push(...gitResult.secrets);
|
|
1868
|
+
allFilesScanned.push(...gitResult.filesScanned);
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
if (options?.scanStaged) {
|
|
1872
|
+
const { isGitRepository: isGitRepository2 } = await import("./git-BZS4DPAI.js");
|
|
1873
|
+
if (await isGitRepository2(cwd)) {
|
|
1874
|
+
const stagedSecrets = await scanStagedFiles(cwd);
|
|
1875
|
+
allSecrets.push(...stagedSecrets);
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
if (options?.diffFrom !== void 0) {
|
|
1879
|
+
const diffSecrets = await scanDiff(options.diffFrom, options?.diffTo, cwd);
|
|
1880
|
+
allSecrets.push(...diffSecrets);
|
|
1881
|
+
}
|
|
1882
|
+
const dedupedSecrets = deduplicateSecrets(allSecrets);
|
|
1883
|
+
const sortedSecrets = sortBySeverity(dedupedSecrets);
|
|
1884
|
+
const scanTimeMs = performance.now() - startTime;
|
|
1885
|
+
return {
|
|
1886
|
+
found: sortedSecrets.length > 0,
|
|
1887
|
+
secrets: sortedSecrets,
|
|
1888
|
+
filesScanned: [...new Set(allFilesScanned)],
|
|
1889
|
+
filesSkipped: [...new Set(allFilesSkipped)],
|
|
1890
|
+
scanTimeMs,
|
|
1891
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1892
|
+
};
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
export {
|
|
1896
|
+
matchPatterns,
|
|
1897
|
+
addCustomPattern,
|
|
1898
|
+
removeCustomPattern,
|
|
1899
|
+
resetPatterns,
|
|
1900
|
+
shannonEntropy,
|
|
1901
|
+
isHighEntropy,
|
|
1902
|
+
scanLineForEntropy,
|
|
1903
|
+
detectHighEntropyStrings,
|
|
1904
|
+
scanFiles,
|
|
1905
|
+
scanStagedFiles,
|
|
1906
|
+
scanDiff,
|
|
1907
|
+
scanGitHistory,
|
|
1908
|
+
formatScanResult,
|
|
1909
|
+
scan
|
|
1910
|
+
};
|