@preship/secrets 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/dist/index.d.ts +79 -0
- package/dist/index.js +777 -0
- package/package.json +26 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { SecretsConfig, SecretsResult, SecretRule } from '@preship/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Scan a project for leaked secrets, API keys, tokens, and credentials.
|
|
5
|
+
*
|
|
6
|
+
* The scanner uses a 3-stage pipeline for each file:
|
|
7
|
+
* Stage 1: Check if file is in allowPaths -- skip entirely
|
|
8
|
+
* Stage 2: Keyword prefilter -- check if any rule keyword exists in file content
|
|
9
|
+
* Stage 3: Run matching rules' regex patterns, check entropy, filter false positives
|
|
10
|
+
*
|
|
11
|
+
* @param projectPath - Absolute path to the project root
|
|
12
|
+
* @param config - Partial secrets configuration (merged with defaults)
|
|
13
|
+
* @returns SecretsResult with findings and statistics
|
|
14
|
+
*/
|
|
15
|
+
declare function scanSecrets(projectPath: string, config?: Partial<SecretsConfig>): Promise<SecretsResult>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get all registered secret detection rules.
|
|
19
|
+
*/
|
|
20
|
+
declare function getAllRules(): SecretRule[];
|
|
21
|
+
/**
|
|
22
|
+
* Get rules whose keywords appear in the given file content.
|
|
23
|
+
*
|
|
24
|
+
* This is the keyword prefilter stage: only rules with at least one
|
|
25
|
+
* matching keyword are returned. Rules with no keywords (like entropy-based
|
|
26
|
+
* rules) are always included since they cannot be prefiltered.
|
|
27
|
+
*
|
|
28
|
+
* @param content - The file content to check keywords against
|
|
29
|
+
* @returns Rules that have at least one keyword match (or no keywords at all)
|
|
30
|
+
*/
|
|
31
|
+
declare function getRulesForKeywords(content: string): SecretRule[];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Shannon entropy calculator for secret detection.
|
|
35
|
+
*
|
|
36
|
+
* Shannon entropy measures the randomness/information density of a string.
|
|
37
|
+
* High-entropy strings are more likely to be secrets (API keys, tokens, etc.)
|
|
38
|
+
* because they contain more random characters compared to natural language.
|
|
39
|
+
*
|
|
40
|
+
* Formula: H = -sum(p(x) * log2(p(x))) for each unique character x
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* Calculate Shannon entropy of a string.
|
|
44
|
+
*
|
|
45
|
+
* @param str - The string to calculate entropy for
|
|
46
|
+
* @returns Shannon entropy value (bits per character). Range: 0 to log2(uniqueChars).
|
|
47
|
+
* Typical thresholds:
|
|
48
|
+
* - English text: ~3.5-4.0
|
|
49
|
+
* - Base64 encoded: ~5.0-6.0
|
|
50
|
+
* - Hex encoded: ~3.5-4.0
|
|
51
|
+
* - True random (base64 charset): ~5.9
|
|
52
|
+
*/
|
|
53
|
+
declare function shannonEntropy(str: string): number;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Walk project directory recursively and collect scannable file paths.
|
|
57
|
+
*
|
|
58
|
+
* @param projectPath - Absolute path to the project root
|
|
59
|
+
* @param allowPaths - Relative paths to skip (glob-style, uses startsWith matching)
|
|
60
|
+
* @param scanPaths - If non-empty, only scan files under these relative paths
|
|
61
|
+
* @returns Array of file paths relative to projectPath
|
|
62
|
+
*/
|
|
63
|
+
declare function walkProjectFiles(projectPath: string, allowPaths: string[], scanPaths: string[]): string[];
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @preship/secrets -- Secrets and credential detection for PreShip
|
|
67
|
+
*
|
|
68
|
+
* Scans project files for leaked API keys, tokens, passwords, private keys,
|
|
69
|
+
* database connection strings, and other credentials. Uses a 3-stage pipeline:
|
|
70
|
+
*
|
|
71
|
+
* 1. File walking with .gitignore-style exclusions
|
|
72
|
+
* 2. Keyword prefiltering for fast file-level skipping
|
|
73
|
+
* 3. Regex pattern matching with entropy analysis and false-positive filtering
|
|
74
|
+
*
|
|
75
|
+
* Zero external dependencies -- uses only Node.js built-in fs, path, and crypto.
|
|
76
|
+
*/
|
|
77
|
+
declare const SECRETS_MODULE_VERSION = "1.0.0";
|
|
78
|
+
|
|
79
|
+
export { SECRETS_MODULE_VERSION, getAllRules, getRulesForKeywords, scanSecrets, shannonEntropy, walkProjectFiles };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,777 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
SECRETS_MODULE_VERSION: () => SECRETS_MODULE_VERSION,
|
|
34
|
+
getAllRules: () => getAllRules,
|
|
35
|
+
getRulesForKeywords: () => getRulesForKeywords,
|
|
36
|
+
scanSecrets: () => scanSecrets,
|
|
37
|
+
shannonEntropy: () => shannonEntropy,
|
|
38
|
+
walkProjectFiles: () => walkProjectFiles
|
|
39
|
+
});
|
|
40
|
+
module.exports = __toCommonJS(index_exports);
|
|
41
|
+
|
|
42
|
+
// src/scanner.ts
|
|
43
|
+
var fs2 = __toESM(require("fs"));
|
|
44
|
+
var path2 = __toESM(require("path"));
|
|
45
|
+
|
|
46
|
+
// src/entropy.ts
|
|
47
|
+
function shannonEntropy(str) {
|
|
48
|
+
if (str.length === 0) {
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
const freq = /* @__PURE__ */ new Map();
|
|
52
|
+
for (const char of str) {
|
|
53
|
+
freq.set(char, (freq.get(char) ?? 0) + 1);
|
|
54
|
+
}
|
|
55
|
+
const len = str.length;
|
|
56
|
+
let entropy = 0;
|
|
57
|
+
for (const count of freq.values()) {
|
|
58
|
+
const p = count / len;
|
|
59
|
+
if (p > 0) {
|
|
60
|
+
entropy -= p * Math.log2(p);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return entropy;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/rules/aws.ts
|
|
67
|
+
var awsRules = [
|
|
68
|
+
{
|
|
69
|
+
id: "aws-access-key",
|
|
70
|
+
description: "AWS Access Key ID",
|
|
71
|
+
severity: "critical",
|
|
72
|
+
keywords: ["AKIA"],
|
|
73
|
+
pattern: /AKIA[0-9A-Z]{16}/,
|
|
74
|
+
allowPatterns: [
|
|
75
|
+
/AKIAIOSFODNN7EXAMPLE/,
|
|
76
|
+
// AWS documentation example key
|
|
77
|
+
/AKIAI44QH8DHBEXAMPLE/
|
|
78
|
+
// Another AWS doc example
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "aws-secret-key",
|
|
83
|
+
description: "AWS Secret Access Key",
|
|
84
|
+
severity: "critical",
|
|
85
|
+
keywords: ["aws"],
|
|
86
|
+
// 40-character base64 string near aws_secret_access_key or similar keywords
|
|
87
|
+
pattern: /(?:aws_secret_access_key|AWS_SECRET_ACCESS_KEY|aws_secret_key)\s*[=:]\s*['"]?([A-Za-z0-9/+=]{40})['"]?/,
|
|
88
|
+
allowPatterns: [
|
|
89
|
+
/wJalrXUtnFEMI\/K7MDENG\/bPxRfiCYEXAMPLEKEY/
|
|
90
|
+
// AWS documentation example
|
|
91
|
+
]
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: "aws-session-token",
|
|
95
|
+
description: "AWS Session Token",
|
|
96
|
+
severity: "high",
|
|
97
|
+
keywords: ["aws_session_token"],
|
|
98
|
+
// Long base64 string near aws_session_token keyword
|
|
99
|
+
pattern: /(?:aws_session_token|AWS_SESSION_TOKEN)\s*[=:]\s*['"]?([A-Za-z0-9/+=]{100,})['"]?/
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: "aws-mws-key",
|
|
103
|
+
description: "Amazon Marketplace Web Service (MWS) Auth Token",
|
|
104
|
+
severity: "critical",
|
|
105
|
+
keywords: ["amzn.mws"],
|
|
106
|
+
pattern: /amzn\.mws\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/
|
|
107
|
+
}
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
// src/rules/gcp.ts
|
|
111
|
+
var gcpRules = [
|
|
112
|
+
{
|
|
113
|
+
id: "gcp-api-key",
|
|
114
|
+
description: "Google Cloud API Key",
|
|
115
|
+
severity: "critical",
|
|
116
|
+
keywords: ["AIza"],
|
|
117
|
+
pattern: /AIza[0-9A-Za-z_-]{35}/,
|
|
118
|
+
allowPatterns: [
|
|
119
|
+
/AIzaSyA1234567890EXAMPLE_KEY_HERE/
|
|
120
|
+
// Placeholder
|
|
121
|
+
]
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: "gcp-service-account",
|
|
125
|
+
description: "Google Cloud Service Account JSON",
|
|
126
|
+
severity: "critical",
|
|
127
|
+
keywords: ["service_account"],
|
|
128
|
+
// Detects the "type": "service_account" pattern in JSON files
|
|
129
|
+
pattern: /"type"\s*:\s*"service_account"/
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: "gcp-oauth-secret",
|
|
133
|
+
description: "Google OAuth Client Secret",
|
|
134
|
+
severity: "high",
|
|
135
|
+
keywords: ["client_secret"],
|
|
136
|
+
// client_secret near googleapis or google context
|
|
137
|
+
pattern: /(?:client_secret|CLIENT_SECRET)\s*[=:"]\s*['"]?([A-Za-z0-9_-]{24,})['"]?/
|
|
138
|
+
}
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
// src/rules/github.ts
|
|
142
|
+
var githubRules = [
|
|
143
|
+
{
|
|
144
|
+
id: "github-pat",
|
|
145
|
+
description: "GitHub Personal Access Token",
|
|
146
|
+
severity: "high",
|
|
147
|
+
keywords: ["ghp_"],
|
|
148
|
+
pattern: /ghp_[0-9a-zA-Z]{36}/
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: "github-oauth",
|
|
152
|
+
description: "GitHub OAuth Access Token",
|
|
153
|
+
severity: "high",
|
|
154
|
+
keywords: ["gho_"],
|
|
155
|
+
pattern: /gho_[0-9a-zA-Z]{36}/
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: "github-app-token",
|
|
159
|
+
description: "GitHub App Token (User-to-Server or Server-to-Server)",
|
|
160
|
+
severity: "high",
|
|
161
|
+
keywords: ["ghu_", "ghs_"],
|
|
162
|
+
pattern: /(?:ghu|ghs)_[0-9a-zA-Z]{36}/
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: "github-fine-grained",
|
|
166
|
+
description: "GitHub Fine-Grained Personal Access Token",
|
|
167
|
+
severity: "high",
|
|
168
|
+
keywords: ["github_pat_"],
|
|
169
|
+
pattern: /github_pat_[0-9a-zA-Z]{22}_[0-9a-zA-Z]{59}/
|
|
170
|
+
}
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
// src/rules/tokens.ts
|
|
174
|
+
var tokenRules = [
|
|
175
|
+
{
|
|
176
|
+
id: "stripe-secret-key",
|
|
177
|
+
description: "Stripe Secret API Key",
|
|
178
|
+
severity: "critical",
|
|
179
|
+
keywords: ["sk_live_"],
|
|
180
|
+
pattern: /sk_live_[0-9a-zA-Z]{24,}/
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
id: "stripe-restricted-key",
|
|
184
|
+
description: "Stripe Restricted API Key",
|
|
185
|
+
severity: "high",
|
|
186
|
+
keywords: ["rk_live_"],
|
|
187
|
+
pattern: /rk_live_[0-9a-zA-Z]{24,}/
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
id: "slack-bot-token",
|
|
191
|
+
description: "Slack Bot Token",
|
|
192
|
+
severity: "high",
|
|
193
|
+
keywords: ["xoxb-"],
|
|
194
|
+
pattern: /xoxb-[0-9]{10,}-[0-9]{10,}-[a-zA-Z0-9]{24}/
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
id: "slack-webhook",
|
|
198
|
+
description: "Slack Incoming Webhook URL",
|
|
199
|
+
severity: "high",
|
|
200
|
+
keywords: ["hooks.slack.com"],
|
|
201
|
+
pattern: /https:\/\/hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[a-zA-Z0-9]+/
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
id: "twilio-api-key",
|
|
205
|
+
description: "Twilio API Key",
|
|
206
|
+
severity: "high",
|
|
207
|
+
keywords: ["twilio", "SK"],
|
|
208
|
+
pattern: /SK[0-9a-fA-F]{32}/
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
id: "sendgrid-api-key",
|
|
212
|
+
description: "SendGrid API Key",
|
|
213
|
+
severity: "high",
|
|
214
|
+
keywords: ["SG."],
|
|
215
|
+
pattern: /SG\.[0-9A-Za-z_-]{22}\.[0-9A-Za-z_-]{43}/
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
id: "npm-token",
|
|
219
|
+
description: "npm Access Token",
|
|
220
|
+
severity: "high",
|
|
221
|
+
keywords: ["npm_"],
|
|
222
|
+
pattern: /npm_[0-9a-zA-Z]{36}/
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
id: "mailchimp-api-key",
|
|
226
|
+
description: "Mailchimp API Key",
|
|
227
|
+
severity: "medium",
|
|
228
|
+
keywords: ["mailchimp", "-us"],
|
|
229
|
+
pattern: /[0-9a-f]{32}-us[0-9]{1,2}/
|
|
230
|
+
}
|
|
231
|
+
];
|
|
232
|
+
|
|
233
|
+
// src/rules/private-keys.ts
|
|
234
|
+
var privateKeyRules = [
|
|
235
|
+
{
|
|
236
|
+
id: "rsa-private-key",
|
|
237
|
+
description: "RSA Private Key",
|
|
238
|
+
severity: "critical",
|
|
239
|
+
keywords: ["BEGIN RSA PRIVATE KEY"],
|
|
240
|
+
pattern: /-----BEGIN RSA PRIVATE KEY-----/
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
id: "dsa-private-key",
|
|
244
|
+
description: "DSA Private Key",
|
|
245
|
+
severity: "critical",
|
|
246
|
+
keywords: ["BEGIN DSA PRIVATE KEY"],
|
|
247
|
+
pattern: /-----BEGIN DSA PRIVATE KEY-----/
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
id: "ec-private-key",
|
|
251
|
+
description: "EC Private Key",
|
|
252
|
+
severity: "critical",
|
|
253
|
+
keywords: ["BEGIN EC PRIVATE KEY"],
|
|
254
|
+
pattern: /-----BEGIN EC PRIVATE KEY-----/
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
id: "pgp-private-key",
|
|
258
|
+
description: "PGP Private Key Block",
|
|
259
|
+
severity: "critical",
|
|
260
|
+
keywords: ["BEGIN PGP PRIVATE KEY BLOCK"],
|
|
261
|
+
pattern: /-----BEGIN PGP PRIVATE KEY BLOCK-----/
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
id: "ssh-private-key",
|
|
265
|
+
description: "OpenSSH Private Key",
|
|
266
|
+
severity: "critical",
|
|
267
|
+
keywords: ["BEGIN OPENSSH PRIVATE KEY"],
|
|
268
|
+
pattern: /-----BEGIN OPENSSH PRIVATE KEY-----/
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
id: "pkcs8-private-key",
|
|
272
|
+
description: "PKCS#8 Private Key",
|
|
273
|
+
severity: "critical",
|
|
274
|
+
keywords: ["BEGIN PRIVATE KEY"],
|
|
275
|
+
pattern: /-----BEGIN PRIVATE KEY-----/
|
|
276
|
+
}
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
// src/rules/database.ts
|
|
280
|
+
var databaseRules = [
|
|
281
|
+
{
|
|
282
|
+
id: "postgres-uri",
|
|
283
|
+
description: "PostgreSQL Connection URI with Password",
|
|
284
|
+
severity: "critical",
|
|
285
|
+
keywords: ["postgres"],
|
|
286
|
+
// postgres:// or postgresql:// with user:password@host
|
|
287
|
+
pattern: /postgres(?:ql)?:\/\/[^\s:]+:[^\s@]+@[^\s]+/,
|
|
288
|
+
allowPatterns: [
|
|
289
|
+
/postgres(?:ql)?:\/\/user(?:name)?:password@/,
|
|
290
|
+
// Common placeholder
|
|
291
|
+
/postgres(?:ql)?:\/\/postgres:postgres@localhost/
|
|
292
|
+
// Local dev default
|
|
293
|
+
]
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
id: "mysql-uri",
|
|
297
|
+
description: "MySQL Connection URI with Password",
|
|
298
|
+
severity: "critical",
|
|
299
|
+
keywords: ["mysql"],
|
|
300
|
+
// mysql:// with user:password@host
|
|
301
|
+
pattern: /mysql:\/\/[^\s:]+:[^\s@]+@[^\s]+/,
|
|
302
|
+
allowPatterns: [
|
|
303
|
+
/mysql:\/\/user(?:name)?:password@/,
|
|
304
|
+
// Common placeholder
|
|
305
|
+
/mysql:\/\/root:root@localhost/,
|
|
306
|
+
// Local dev default
|
|
307
|
+
/mysql:\/\/root:password@localhost/
|
|
308
|
+
// Local dev default
|
|
309
|
+
]
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
id: "mongodb-uri",
|
|
313
|
+
description: "MongoDB Connection URI with Password",
|
|
314
|
+
severity: "critical",
|
|
315
|
+
keywords: ["mongodb"],
|
|
316
|
+
// mongodb:// or mongodb+srv:// with user:password@host
|
|
317
|
+
pattern: /mongodb(?:\+srv)?:\/\/[^\s:]+:[^\s@]+@[^\s]+/,
|
|
318
|
+
allowPatterns: [
|
|
319
|
+
/mongodb(?:\+srv)?:\/\/user(?:name)?:password@/,
|
|
320
|
+
// Common placeholder
|
|
321
|
+
/mongodb(?:\+srv)?:\/\/root:example@/
|
|
322
|
+
// Docker docs example
|
|
323
|
+
]
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
id: "redis-uri",
|
|
327
|
+
description: "Redis Connection URI with Password",
|
|
328
|
+
severity: "high",
|
|
329
|
+
keywords: ["redis"],
|
|
330
|
+
// redis:// or rediss:// with user:password@host
|
|
331
|
+
pattern: /rediss?:\/\/[^\s:]*:[^\s@]+@[^\s]+/,
|
|
332
|
+
allowPatterns: [
|
|
333
|
+
/rediss?:\/\/:password@localhost/
|
|
334
|
+
// Common placeholder
|
|
335
|
+
]
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
id: "database-password",
|
|
339
|
+
description: "Database Password Assignment",
|
|
340
|
+
severity: "high",
|
|
341
|
+
keywords: ["db_password", "database_password", "DB_PASS"],
|
|
342
|
+
// Various database password environment variable patterns
|
|
343
|
+
pattern: /(?:db_password|database_password|DB_PASS(?:WORD)?|DB_PWD)\s*[=:]\s*['"][^'"]{8,}['"]/i,
|
|
344
|
+
allowPatterns: [
|
|
345
|
+
/['"](?:password|changeme|your[_-]?password|replace[_-]?me|TODO|xxx+)['"]/i
|
|
346
|
+
]
|
|
347
|
+
}
|
|
348
|
+
];
|
|
349
|
+
|
|
350
|
+
// src/rules/generic.ts
|
|
351
|
+
var GENERIC_ALLOW_PATTERNS = [
|
|
352
|
+
/['"](?:your[_-]?api[_-]?key(?:[_-]?here)?|INSERT[_-]?(?:TOKEN|KEY|SECRET)[_-]?HERE|xxx+|TODO|CHANGEME|REPLACE[_-]?ME|example|test|dummy|fake|placeholder|sample)['"]/i,
|
|
353
|
+
/['"](?:sk[_-]?test|pk[_-]?test|sk[_-]?example|pk[_-]?example)['"]/i,
|
|
354
|
+
/['"](?:\$\{|<[^>]+>|\{\{)/
|
|
355
|
+
// Template variable references: ${VAR}, <TOKEN>, {{VAR}}
|
|
356
|
+
];
|
|
357
|
+
var genericRules = [
|
|
358
|
+
{
|
|
359
|
+
id: "generic-api-key",
|
|
360
|
+
description: "Generic API Key Assignment",
|
|
361
|
+
severity: "medium",
|
|
362
|
+
keywords: ["api_key", "api-key", "apikey", "API_KEY", "APIKEY"],
|
|
363
|
+
pattern: /(?:api[_-]?key|apikey|API[_-]?KEY|APIKEY)\s*[=:]\s*['"][0-9a-zA-Z]{20,}['"]/i,
|
|
364
|
+
allowPatterns: GENERIC_ALLOW_PATTERNS
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
id: "generic-secret",
|
|
368
|
+
description: "Generic Secret Assignment",
|
|
369
|
+
severity: "medium",
|
|
370
|
+
keywords: ["secret", "SECRET"],
|
|
371
|
+
pattern: /(?:secret|SECRET|client_secret|CLIENT_SECRET|app_secret|APP_SECRET)\s*[=:]\s*['"][0-9a-zA-Z]{20,}['"]/i,
|
|
372
|
+
allowPatterns: GENERIC_ALLOW_PATTERNS
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
id: "generic-password",
|
|
376
|
+
description: "Generic Password Assignment",
|
|
377
|
+
severity: "medium",
|
|
378
|
+
keywords: ["password", "passwd", "pwd", "PASSWORD", "PASSWD", "PWD"],
|
|
379
|
+
pattern: /(?:password|passwd|pwd|PASSWORD|PASSWD|PWD)\s*[=:]\s*['"][^'"]{8,}['"]/,
|
|
380
|
+
allowPatterns: [
|
|
381
|
+
...GENERIC_ALLOW_PATTERNS,
|
|
382
|
+
/['"](?:password|changeme|admin|root|test(?:ing)?|development|12345678)['"]/i
|
|
383
|
+
]
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
id: "generic-token",
|
|
387
|
+
description: "Generic Token Assignment",
|
|
388
|
+
severity: "medium",
|
|
389
|
+
keywords: ["token", "TOKEN", "access_token", "ACCESS_TOKEN"],
|
|
390
|
+
pattern: /(?:token|TOKEN|access_token|ACCESS_TOKEN|auth_token|AUTH_TOKEN)\s*[=:]\s*['"][0-9a-zA-Z]{20,}['"]/i,
|
|
391
|
+
allowPatterns: GENERIC_ALLOW_PATTERNS
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
id: "jwt-token",
|
|
395
|
+
description: "JSON Web Token (JWT)",
|
|
396
|
+
severity: "medium",
|
|
397
|
+
keywords: ["eyJ"],
|
|
398
|
+
// JWT structure: base64url.base64url.signature
|
|
399
|
+
pattern: /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]+/
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
id: "env-file",
|
|
403
|
+
description: "Environment Configuration File with Secrets",
|
|
404
|
+
severity: "high",
|
|
405
|
+
keywords: ["_KEY=", "_SECRET=", "_TOKEN=", "_PASSWORD="],
|
|
406
|
+
// Detects KEY=value, SECRET=value patterns in .env-style files
|
|
407
|
+
// This rule is special: the scanner should also check if the filename is .env*
|
|
408
|
+
pattern: /(?:_KEY|_SECRET|_TOKEN|_PASSWORD|_API_KEY)\s*=\s*[^\s#]{8,}/,
|
|
409
|
+
allowPatterns: [
|
|
410
|
+
/=\s*(?:your[_-]?|INSERT|TODO|CHANGEME|REPLACE|<|"\$\{|\$\(|%)/i
|
|
411
|
+
]
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
id: "high-entropy-base64",
|
|
415
|
+
description: "High-Entropy Base64 String (possible secret)",
|
|
416
|
+
severity: "low",
|
|
417
|
+
keywords: [],
|
|
418
|
+
// No keyword prefilter; this rule is opt-in via entropy threshold
|
|
419
|
+
pattern: /[A-Za-z0-9+/]{40,}={0,2}/,
|
|
420
|
+
entropyThreshold: 4.5,
|
|
421
|
+
allowPatterns: [
|
|
422
|
+
/^[A-Z]+$/,
|
|
423
|
+
// All uppercase (likely a constant name)
|
|
424
|
+
/^[a-z]+$/,
|
|
425
|
+
// All lowercase (likely a word)
|
|
426
|
+
/[A-Za-z]{40,}/
|
|
427
|
+
// Long alphabetic-only strings are typically not secrets
|
|
428
|
+
]
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
id: "high-entropy-hex",
|
|
432
|
+
description: "High-Entropy Hex String (possible secret)",
|
|
433
|
+
severity: "low",
|
|
434
|
+
keywords: [],
|
|
435
|
+
// No keyword prefilter; this rule is opt-in via entropy threshold
|
|
436
|
+
pattern: /[0-9a-fA-F]{40,}/,
|
|
437
|
+
entropyThreshold: 3,
|
|
438
|
+
allowPatterns: [
|
|
439
|
+
/^0{10,}$/,
|
|
440
|
+
// All zeros (placeholder/null hash)
|
|
441
|
+
/^[fF]{10,}$/
|
|
442
|
+
// All f's (mask value)
|
|
443
|
+
]
|
|
444
|
+
}
|
|
445
|
+
];
|
|
446
|
+
|
|
447
|
+
// src/rules/index.ts
|
|
448
|
+
var ALL_RULES = [
|
|
449
|
+
...awsRules,
|
|
450
|
+
...gcpRules,
|
|
451
|
+
...githubRules,
|
|
452
|
+
...tokenRules,
|
|
453
|
+
...privateKeyRules,
|
|
454
|
+
...databaseRules,
|
|
455
|
+
...genericRules
|
|
456
|
+
];
|
|
457
|
+
function getAllRules() {
|
|
458
|
+
return ALL_RULES;
|
|
459
|
+
}
|
|
460
|
+
function getRulesForKeywords(content) {
|
|
461
|
+
return ALL_RULES.filter((rule) => {
|
|
462
|
+
if (rule.keywords.length === 0) {
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
465
|
+
return rule.keywords.some((keyword) => content.includes(keyword));
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// src/walker.ts
|
|
470
|
+
var fs = __toESM(require("fs"));
|
|
471
|
+
var path = __toESM(require("path"));
|
|
472
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
473
|
+
"node_modules",
|
|
474
|
+
".git",
|
|
475
|
+
"dist",
|
|
476
|
+
"build",
|
|
477
|
+
"coverage",
|
|
478
|
+
"__pycache__",
|
|
479
|
+
".next",
|
|
480
|
+
".nuxt",
|
|
481
|
+
".cache",
|
|
482
|
+
".turbo",
|
|
483
|
+
".output"
|
|
484
|
+
]);
|
|
485
|
+
var SKIP_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
486
|
+
".png",
|
|
487
|
+
".jpg",
|
|
488
|
+
".jpeg",
|
|
489
|
+
".gif",
|
|
490
|
+
".ico",
|
|
491
|
+
".svg",
|
|
492
|
+
".webp",
|
|
493
|
+
".bmp",
|
|
494
|
+
".tiff",
|
|
495
|
+
".woff",
|
|
496
|
+
".woff2",
|
|
497
|
+
".ttf",
|
|
498
|
+
".eot",
|
|
499
|
+
".otf",
|
|
500
|
+
".pdf",
|
|
501
|
+
".zip",
|
|
502
|
+
".tar",
|
|
503
|
+
".gz",
|
|
504
|
+
".bz2",
|
|
505
|
+
".xz",
|
|
506
|
+
".7z",
|
|
507
|
+
".rar",
|
|
508
|
+
".jar",
|
|
509
|
+
".war",
|
|
510
|
+
".exe",
|
|
511
|
+
".dll",
|
|
512
|
+
".so",
|
|
513
|
+
".dylib",
|
|
514
|
+
".bin",
|
|
515
|
+
".o",
|
|
516
|
+
".obj",
|
|
517
|
+
".class",
|
|
518
|
+
".pyc",
|
|
519
|
+
".pyo",
|
|
520
|
+
".wasm",
|
|
521
|
+
".mp3",
|
|
522
|
+
".mp4",
|
|
523
|
+
".avi",
|
|
524
|
+
".mov",
|
|
525
|
+
".mkv",
|
|
526
|
+
".flac",
|
|
527
|
+
".wav"
|
|
528
|
+
]);
|
|
529
|
+
var ALLOWED_LOCKFILES = /* @__PURE__ */ new Set([
|
|
530
|
+
"package-lock.json",
|
|
531
|
+
"yarn.lock",
|
|
532
|
+
"pnpm-lock.yaml"
|
|
533
|
+
]);
|
|
534
|
+
var MAX_FILE_SIZE = 1048576;
|
|
535
|
+
function walkProjectFiles(projectPath, allowPaths, scanPaths) {
|
|
536
|
+
const files = [];
|
|
537
|
+
const normalizedScanPaths = scanPaths.map((p) => p.replace(/\/+$/, ""));
|
|
538
|
+
const normalizedAllowPaths = allowPaths.map((p) => p.replace(/\/+$/, ""));
|
|
539
|
+
walkDirectory(projectPath, "", files, normalizedAllowPaths, normalizedScanPaths);
|
|
540
|
+
return files;
|
|
541
|
+
}
|
|
542
|
+
function walkDirectory(rootPath, relativePath, files, allowPaths, scanPaths) {
|
|
543
|
+
const absolutePath = relativePath ? path.join(rootPath, relativePath) : rootPath;
|
|
544
|
+
let entries;
|
|
545
|
+
try {
|
|
546
|
+
entries = fs.readdirSync(absolutePath, { withFileTypes: true });
|
|
547
|
+
} catch {
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
for (const entry of entries) {
|
|
551
|
+
const entryRelative = relativePath ? path.join(relativePath, entry.name) : entry.name;
|
|
552
|
+
if (entry.isDirectory()) {
|
|
553
|
+
if (SKIP_DIRS.has(entry.name)) {
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
if (isAllowedPath(entryRelative, allowPaths)) {
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (scanPaths.length > 0 && !isUnderOrContainsScanPath(entryRelative, scanPaths)) {
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
walkDirectory(rootPath, entryRelative, files, allowPaths, scanPaths);
|
|
563
|
+
} else if (entry.isFile()) {
|
|
564
|
+
if (isAllowedPath(entryRelative, allowPaths)) {
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
if (scanPaths.length > 0 && !isUnderScanPath(entryRelative, scanPaths)) {
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
571
|
+
if (SKIP_EXTENSIONS.has(ext)) {
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
if (ext === ".lock" && !ALLOWED_LOCKFILES.has(entry.name)) {
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
try {
|
|
578
|
+
const stats = fs.statSync(path.join(rootPath, entryRelative));
|
|
579
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
} catch {
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
files.push(entryRelative);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
function isAllowedPath(relativePath, allowPaths) {
|
|
590
|
+
return allowPaths.some(
|
|
591
|
+
(allowed) => relativePath === allowed || relativePath.startsWith(allowed + path.sep) || relativePath.startsWith(allowed + "/")
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
function isUnderScanPath(relativePath, scanPaths) {
|
|
595
|
+
return scanPaths.some((scanPath) => {
|
|
596
|
+
if (scanPath === "" || scanPath === ".") return true;
|
|
597
|
+
return relativePath === scanPath || relativePath.startsWith(scanPath + path.sep) || relativePath.startsWith(scanPath + "/");
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
function isUnderOrContainsScanPath(dirPath, scanPaths) {
|
|
601
|
+
return scanPaths.some((scanPath) => {
|
|
602
|
+
if (scanPath === "" || scanPath === ".") return true;
|
|
603
|
+
if (scanPath.startsWith(dirPath + path.sep) || scanPath.startsWith(dirPath + "/")) {
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
if (dirPath === scanPath || dirPath.startsWith(scanPath + path.sep) || dirPath.startsWith(scanPath + "/")) {
|
|
607
|
+
return true;
|
|
608
|
+
}
|
|
609
|
+
return false;
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// src/scanner.ts
|
|
614
|
+
var DEFAULT_SECRETS_CONFIG = {
|
|
615
|
+
scanPaths: [],
|
|
616
|
+
allowPaths: [],
|
|
617
|
+
allowRules: []
|
|
618
|
+
};
|
|
619
|
+
async function scanSecrets(projectPath, config = {}) {
|
|
620
|
+
const startTime = Date.now();
|
|
621
|
+
const mergedConfig = {
|
|
622
|
+
...DEFAULT_SECRETS_CONFIG,
|
|
623
|
+
...config
|
|
624
|
+
};
|
|
625
|
+
const allRules = getAllRules().filter(
|
|
626
|
+
(rule) => !mergedConfig.allowRules.includes(rule.id)
|
|
627
|
+
);
|
|
628
|
+
const files = walkProjectFiles(
|
|
629
|
+
projectPath,
|
|
630
|
+
mergedConfig.allowPaths,
|
|
631
|
+
mergedConfig.scanPaths
|
|
632
|
+
);
|
|
633
|
+
const findings = [];
|
|
634
|
+
let filesScanned = 0;
|
|
635
|
+
for (const relativePath of files) {
|
|
636
|
+
const absolutePath = path2.join(projectPath, relativePath);
|
|
637
|
+
let content;
|
|
638
|
+
try {
|
|
639
|
+
content = fs2.readFileSync(absolutePath, "utf-8");
|
|
640
|
+
} catch {
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
filesScanned++;
|
|
644
|
+
const isEnvFile = isEnvironmentFile(relativePath);
|
|
645
|
+
const candidateRules = getRulesForKeywords(content).filter(
|
|
646
|
+
(rule) => !mergedConfig.allowRules.includes(rule.id)
|
|
647
|
+
);
|
|
648
|
+
if (candidateRules.length === 0 && !isEnvFile) {
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
const lines = content.split("\n");
|
|
652
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
653
|
+
const line = lines[lineIndex];
|
|
654
|
+
for (const rule of candidateRules) {
|
|
655
|
+
scanLineForRule(rule, line, lineIndex, relativePath, findings);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (isEnvFile && hasEnvSecretContent(content)) {
|
|
659
|
+
const envRule = allRules.find((r) => r.id === "env-file");
|
|
660
|
+
if (envRule && !mergedConfig.allowRules.includes("env-file")) {
|
|
661
|
+
const hasEnvFindings = findings.some(
|
|
662
|
+
(f) => f.ruleId === "env-file" && f.file === relativePath
|
|
663
|
+
);
|
|
664
|
+
if (!hasEnvFindings) {
|
|
665
|
+
findings.push({
|
|
666
|
+
ruleId: "env-file",
|
|
667
|
+
description: "Environment file with potential secrets detected",
|
|
668
|
+
severity: "high",
|
|
669
|
+
file: relativePath,
|
|
670
|
+
line: 1,
|
|
671
|
+
column: 1,
|
|
672
|
+
match: path2.basename(relativePath)
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
const stats = {
|
|
679
|
+
critical: findings.filter((f) => f.severity === "critical").length,
|
|
680
|
+
high: findings.filter((f) => f.severity === "high").length,
|
|
681
|
+
medium: findings.filter((f) => f.severity === "medium").length,
|
|
682
|
+
low: findings.filter((f) => f.severity === "low").length
|
|
683
|
+
};
|
|
684
|
+
const passed = stats.critical === 0 && stats.high === 0;
|
|
685
|
+
return {
|
|
686
|
+
enabled: true,
|
|
687
|
+
passed,
|
|
688
|
+
filesScanned,
|
|
689
|
+
findings,
|
|
690
|
+
stats,
|
|
691
|
+
scanDurationMs: Date.now() - startTime
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
function scanLineForRule(rule, line, lineIndex, relativePath, findings) {
|
|
695
|
+
const regex = new RegExp(rule.pattern.source, rule.pattern.flags.replace("g", ""));
|
|
696
|
+
const match = regex.exec(line);
|
|
697
|
+
if (!match) {
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
const matchedString = match[0];
|
|
701
|
+
const secretValue = match[1] ?? matchedString;
|
|
702
|
+
if (rule.allowPatterns && rule.allowPatterns.length > 0) {
|
|
703
|
+
const isAllowed = rule.allowPatterns.some(
|
|
704
|
+
(allowPattern) => allowPattern.test(matchedString)
|
|
705
|
+
);
|
|
706
|
+
if (isAllowed) {
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
let entropy;
|
|
711
|
+
if (rule.entropyThreshold !== void 0) {
|
|
712
|
+
entropy = shannonEntropy(secretValue);
|
|
713
|
+
if (entropy < rule.entropyThreshold) {
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
findings.push({
|
|
718
|
+
ruleId: rule.id,
|
|
719
|
+
description: rule.description,
|
|
720
|
+
severity: rule.severity,
|
|
721
|
+
file: relativePath,
|
|
722
|
+
line: lineIndex + 1,
|
|
723
|
+
// 1-indexed
|
|
724
|
+
column: match.index + 1,
|
|
725
|
+
// 1-indexed
|
|
726
|
+
match: redactSecret(matchedString, rule),
|
|
727
|
+
entropy
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
function redactSecret(value, rule) {
|
|
731
|
+
if (rule.id.endsWith("-private-key")) {
|
|
732
|
+
return value;
|
|
733
|
+
}
|
|
734
|
+
if (rule.id === "gcp-service-account") {
|
|
735
|
+
return '"type": "service_account"';
|
|
736
|
+
}
|
|
737
|
+
if (value.length < 12) {
|
|
738
|
+
return value.substring(0, 2) + "****";
|
|
739
|
+
}
|
|
740
|
+
return value.substring(0, 4) + "****" + value.substring(value.length - 4);
|
|
741
|
+
}
|
|
742
|
+
function isEnvironmentFile(relativePath) {
|
|
743
|
+
const basename2 = path2.basename(relativePath);
|
|
744
|
+
return basename2 === ".env" || basename2.startsWith(".env.") || basename2 === ".env.local" || basename2 === ".env.development" || basename2 === ".env.production" || basename2 === ".env.staging" || basename2 === ".env.test";
|
|
745
|
+
}
|
|
746
|
+
function hasEnvSecretContent(content) {
|
|
747
|
+
const lines = content.split("\n");
|
|
748
|
+
for (const line of lines) {
|
|
749
|
+
const trimmed = line.trim();
|
|
750
|
+
if (trimmed === "" || trimmed.startsWith("#")) {
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
const eqIndex = trimmed.indexOf("=");
|
|
754
|
+
if (eqIndex > 0) {
|
|
755
|
+
const value = trimmed.substring(eqIndex + 1).trim().replace(/^['"]|['"]$/g, "");
|
|
756
|
+
if (value === "" || value.startsWith("${") || value.startsWith("<") || value.startsWith("{{") || /^(your[_-]|INSERT|TODO|CHANGEME|REPLACE|xxx)/i.test(value)) {
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
if (value.length >= 8) {
|
|
760
|
+
return true;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// src/index.ts
|
|
768
|
+
var SECRETS_MODULE_VERSION = "1.0.0";
|
|
769
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
770
|
+
0 && (module.exports = {
|
|
771
|
+
SECRETS_MODULE_VERSION,
|
|
772
|
+
getAllRules,
|
|
773
|
+
getRulesForKeywords,
|
|
774
|
+
scanSecrets,
|
|
775
|
+
shannonEntropy,
|
|
776
|
+
walkProjectFiles
|
|
777
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@preship/secrets",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Secrets detection for PreShip — find leaked API keys, tokens, and credentials before shipping",
|
|
5
|
+
"author": "Cyfox Inc.",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"require": "./dist/index.js",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup src/index.ts --format cjs --dts --clean",
|
|
21
|
+
"lint": "tsc --noEmit"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@preship/core": "2.0.0"
|
|
25
|
+
}
|
|
26
|
+
}
|