nerdsystem 0.0.5 → 0.0.7
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/README.md +12 -1
- package/dist/api.js +4 -3
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +311 -0
- package/dist/commands/admin.d.ts +10 -0
- package/dist/commands/admin.js +36 -0
- package/dist/commands/check.d.ts +6 -1
- package/dist/commands/check.js +28 -10
- package/dist/commands/db.d.ts +8 -0
- package/dist/commands/db.js +25 -0
- package/dist/commands/deploy.js +5 -3
- package/dist/commands/dev.js +2 -1
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/github.d.ts +1 -0
- package/dist/commands/github.js +2 -2
- package/dist/commands/init.js +76 -6
- package/dist/commands/login.js +1 -1
- package/dist/commands/ops.d.ts +1 -0
- package/dist/commands/ops.js +3 -3
- package/dist/commands/org.js +1 -1
- package/dist/commands/trash.d.ts +17 -0
- package/dist/commands/trash.js +84 -0
- package/dist/commands/verify.js +3 -3
- package/dist/gates/code.d.ts +4 -1
- package/dist/gates/code.js +8 -5
- package/dist/gates/fs.js +1 -1
- package/dist/gates/index.d.ts +2 -1
- package/dist/gates/index.js +1 -0
- package/dist/gates/rules.js +6 -3
- package/dist/gates/secrets.d.ts +5 -0
- package/dist/gates/secrets.js +50 -10
- package/dist/gates/text.d.ts +39 -0
- package/dist/gates/text.js +82 -2
- package/dist/gates/types.d.ts +12 -0
- package/dist/gates/types.js +18 -0
- package/dist/index.js +2 -267
- package/dist/util.d.ts +14 -1
- package/dist/util.js +20 -1
- package/package.json +1 -1
- package/template/.claude/skills/nerdsystem/SKILL.md +12 -5
- package/template/.cursor/rules/nerdsystem.mdc +13 -6
- package/template/.github/copilot-instructions.md +13 -6
- package/template/AGENTS.md +13 -6
- package/template/GEMINI.md +13 -6
- package/template/package.json +6 -0
- package/template/src/client/auth.tsx +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
1
2
|
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -41,7 +42,7 @@ export async function linkProject(root, args) {
|
|
|
41
42
|
log.ok(`Linked to existing project ${org}/${args.slug}`);
|
|
42
43
|
}
|
|
43
44
|
catch (err) {
|
|
44
|
-
if (!args.create || !(err instanceof CliError) ||
|
|
45
|
+
if (!args.create || !(err instanceof CliError) || err.code !== "not_found")
|
|
45
46
|
throw err;
|
|
46
47
|
log.step(`Creating project ${org}/${args.slug} (provisions a database and a bucket)`);
|
|
47
48
|
project = (await api.post(`/v1/orgs/${org}/projects`, { slug: args.slug, name: args.name })).project;
|
|
@@ -69,6 +70,50 @@ function seedDescription(root, name, description) {
|
|
|
69
70
|
const heading = `# ${name}\n\n${description ? `${description}\n\n` : "(Describe what this app does here; the agent starts from it.)\n\n"}`;
|
|
70
71
|
writeFileSync(p, heading + readFileSync(p, "utf8").replace(/^# This app runs on nerdsystem\n/, "## This app runs on nerdsystem\n"));
|
|
71
72
|
}
|
|
73
|
+
/** Ten lines a person can read before opening the agent. */
|
|
74
|
+
function writeReadme(root, name, description) {
|
|
75
|
+
const lines = [
|
|
76
|
+
`# ${name}`,
|
|
77
|
+
"",
|
|
78
|
+
description ?? "An app built with a coding agent and deployed on nerdsystem.",
|
|
79
|
+
"",
|
|
80
|
+
"- `nerd dev` runs it locally with real sign-in.",
|
|
81
|
+
"- `nerd check` runs every gate a deploy runs (secrets, dependencies, platform rules, types).",
|
|
82
|
+
"- `nerd deploy` checks, builds, migrates, and ships it; the URL is printed at the end.",
|
|
83
|
+
"",
|
|
84
|
+
"Guides for each coding agent: https://nerdsystem.com/guides",
|
|
85
|
+
"",
|
|
86
|
+
];
|
|
87
|
+
writeFileSync(path.join(root, "README.md"), lines.join("\n"));
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* A repository from the first minute: deploy-on-push and the agent's commits both expect one. Skipped
|
|
91
|
+
* silently when git is missing, and when the folder already sits inside a repository.
|
|
92
|
+
*/
|
|
93
|
+
function initGitRepo(root) {
|
|
94
|
+
try {
|
|
95
|
+
try {
|
|
96
|
+
execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: root, stdio: "ignore" });
|
|
97
|
+
return; // already a repository, or inside one
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// not a repository yet
|
|
101
|
+
}
|
|
102
|
+
execFileSync("git", ["init", "-q"], { cwd: root, stdio: "ignore" });
|
|
103
|
+
execFileSync("git", ["add", "-A"], { cwd: root, stdio: "ignore" });
|
|
104
|
+
const commit = ["commit", "-q", "-m", "Start from the nerdsystem template"];
|
|
105
|
+
try {
|
|
106
|
+
execFileSync("git", commit, { cwd: root, stdio: "ignore" });
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// No identity configured on this machine yet: commit under a placeholder rather than leave the tree uncommitted.
|
|
110
|
+
execFileSync("git", ["-c", "user.name=nerd", "-c", "user.email=nerd@nerdsystem.com", ...commit], { cwd: root, stdio: "ignore" });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// git missing or not configured: fine, the person can init later
|
|
115
|
+
}
|
|
116
|
+
}
|
|
72
117
|
export async function init(dir, args) {
|
|
73
118
|
if (!dir)
|
|
74
119
|
throw new CliError("Usage: nerd init <directory> [--org slug] [--name 'My App'] [--slug my-app]");
|
|
@@ -77,8 +122,19 @@ export async function init(dir, args) {
|
|
|
77
122
|
if (!SLUG_RE.test(slug))
|
|
78
123
|
throw new CliError(`"${slug}" is not a valid project slug (lowercase letters, digits, hyphens). Pass --slug.`);
|
|
79
124
|
const name = args.name ?? slug;
|
|
80
|
-
if (existsSync(root) && readdirSync(root).length > 0)
|
|
81
|
-
throw new CliError(`${root} is not empty.`);
|
|
125
|
+
if (existsSync(root) && readdirSync(root).length > 0) {
|
|
126
|
+
throw new CliError(`${root} is not empty. If an earlier \`nerd init\` stopped partway, run \`nerd link --slug ${slug}\` inside it instead; otherwise pick a new folder name.`);
|
|
127
|
+
}
|
|
128
|
+
// Everything that can fail remotely is checked before a single file is written, so a failure never
|
|
129
|
+
// leaves a half-made folder behind.
|
|
130
|
+
if (!args["no-create"]) {
|
|
131
|
+
const api = ApiClient.fromConfig(); // throws "Not logged in" with the fix
|
|
132
|
+
const me = await api.get("/v1/me");
|
|
133
|
+
if (me.orgs.length === 0)
|
|
134
|
+
throw new CliError("Your account has no organization yet. Sign in to the dashboard once, or ask the person who invited you for the link.");
|
|
135
|
+
if (args.org && !me.orgs.some((o) => o.slug === args.org))
|
|
136
|
+
throw new CliError(`You are not in an org called "${args.org}". Yours: ${me.orgs.map((o) => o.slug).join(", ")}`);
|
|
137
|
+
}
|
|
82
138
|
const src = templateDir();
|
|
83
139
|
log.step(`Copying template into ${root}`);
|
|
84
140
|
mkdirSync(root, { recursive: true });
|
|
@@ -87,6 +143,7 @@ export async function init(dir, args) {
|
|
|
87
143
|
// The published template carries ".gitignore" as "gitignore" (npm strips the dotted name from packages).
|
|
88
144
|
if (existsSync(path.join(root, "gitignore")) && !existsSync(path.join(root, ".gitignore")))
|
|
89
145
|
renameSync(path.join(root, "gitignore"), path.join(root, ".gitignore"));
|
|
146
|
+
const description = args.describe ?? (await askDescription());
|
|
90
147
|
// Rename the app and point the SDK dependency somewhere installable.
|
|
91
148
|
const pkgPath = path.join(root, "package.json");
|
|
92
149
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
@@ -98,10 +155,23 @@ export async function init(dir, args) {
|
|
|
98
155
|
const p = path.join(root, f);
|
|
99
156
|
writeFileSync(p, readFileSync(p, "utf8").replaceAll("nerd-app", slug));
|
|
100
157
|
}
|
|
101
|
-
|
|
158
|
+
// The visible brand in the client: the display name, unless it would break JSX text.
|
|
159
|
+
const display = /[<>{}]/.test(name) ? slug : name;
|
|
160
|
+
const appTsx = path.join(root, "src/client/App.tsx");
|
|
161
|
+
if (existsSync(appTsx))
|
|
162
|
+
writeFileSync(appTsx, readFileSync(appTsx, "utf8").replaceAll("nerd-app", display));
|
|
163
|
+
writeReadme(root, name, description);
|
|
102
164
|
seedDescription(root, name, description);
|
|
103
|
-
|
|
165
|
+
let project;
|
|
166
|
+
try {
|
|
167
|
+
project = await linkProject(root, { ...(args.org !== undefined ? { org: args.org } : {}), slug, name, create: !args["no-create"] });
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
171
|
+
throw new CliError(`The folder is ready but the project was not created: ${reason}\nFix that, then run: cd ${dir} && nerd link --slug ${slug}${args.org ? ` --org ${args.org}` : ""}`);
|
|
172
|
+
}
|
|
173
|
+
initGitRepo(root);
|
|
104
174
|
log.ok(`Ready. Next:\n cd ${dir}\n pnpm install\n nerd dev\n\nThen open the folder in your coding agent and ask it to build the first feature; the nerdsystem skill walks it through.\nDeploys go to ${project.url}`);
|
|
105
175
|
}
|
|
106
176
|
// Kept in sync with packages/sdk/package.json by the release script.
|
|
107
|
-
const SDK_VERSION = "0.0.
|
|
177
|
+
const SDK_VERSION = "0.0.7";
|
package/dist/commands/login.js
CHANGED
|
@@ -28,7 +28,7 @@ export async function login(args) {
|
|
|
28
28
|
else {
|
|
29
29
|
log.info(`Open this link in your browser and approve code ${start.userCode}:\n\n ${start.verificationUrl}\n`);
|
|
30
30
|
if (args.email)
|
|
31
|
-
log.info(`An email
|
|
31
|
+
log.info(`An email went to ${args.email}: one click signs you in (creating your account if you are new) and approves.`);
|
|
32
32
|
log.info("Waiting for approval…");
|
|
33
33
|
}
|
|
34
34
|
}
|
package/dist/commands/ops.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ export declare function secretList(opts: {
|
|
|
23
23
|
/** Value from the argument, or from stdin when omitted, so keys stay out of shell history. */
|
|
24
24
|
export declare function secretSet(name: string | undefined, value: string | undefined, opts: {
|
|
25
25
|
json: boolean;
|
|
26
|
+
previews?: boolean;
|
|
26
27
|
}): Promise<void>;
|
|
27
28
|
export declare function secretRemove(name: string | undefined, opts: {
|
|
28
29
|
json: boolean;
|
package/dist/commands/ops.js
CHANGED
|
@@ -81,13 +81,13 @@ export async function secretList(opts) {
|
|
|
81
81
|
return printJson(r);
|
|
82
82
|
if (!r.secrets.length)
|
|
83
83
|
return log.info("No app secrets yet. Set one with: nerd secret set STRIPE_SECRET_KEY");
|
|
84
|
-
table(r.secrets.map((s) => ({ name: s.name, updated: s.updatedAt })), ["name", "updated"]);
|
|
84
|
+
table(r.secrets.map((s) => ({ name: s.name, previews: s.scope === "all" ? "yes" : "no", updated: s.updatedAt })), ["name", "previews", "updated"]);
|
|
85
85
|
log.info(`\n${r.note}`);
|
|
86
86
|
}
|
|
87
87
|
/** Value from the argument, or from stdin when omitted, so keys stay out of shell history. */
|
|
88
88
|
export async function secretSet(name, value, opts) {
|
|
89
89
|
if (!name)
|
|
90
|
-
throw new CliError("Usage: nerd secret set NAME [value] (omit the value to read it from stdin)");
|
|
90
|
+
throw new CliError("Usage: nerd secret set NAME [value] [--previews] (omit the value to read it from stdin; --previews also gives it to pull request previews)");
|
|
91
91
|
let v = value;
|
|
92
92
|
if (v === undefined) {
|
|
93
93
|
if (process.stdin.isTTY)
|
|
@@ -97,7 +97,7 @@ export async function secretSet(name, value, opts) {
|
|
|
97
97
|
if (!v)
|
|
98
98
|
throw new CliError("Empty value.");
|
|
99
99
|
const nerd = readNerdJson(findProjectRoot());
|
|
100
|
-
const r = await ApiClient.fromConfig().put(projectPath(nerd, `/secrets/${name}`), { value: v });
|
|
100
|
+
const r = await ApiClient.fromConfig().put(projectPath(nerd, `/secrets/${name}`), { value: v, scope: opts.previews ? "all" : "production" });
|
|
101
101
|
if (opts.json)
|
|
102
102
|
return printJson(r);
|
|
103
103
|
log.ok(`Set ${r.secret.name}. ${r.note} For local dev, add ${name}=... to .dev.vars (never commit it).`);
|
package/dist/commands/org.js
CHANGED
|
@@ -76,7 +76,7 @@ export async function logs(opts) {
|
|
|
76
76
|
if (!r.events.length)
|
|
77
77
|
return log.info(`No log events for ${r.scriptName} in the last ${opts.since ?? "30m"}.`);
|
|
78
78
|
for (const e of [...r.events].reverse())
|
|
79
|
-
process.stdout.write(`${e.timestamp} ${e.level.padEnd(5)} ${e.context ? `${e.context} ` : ""}${e.message}\n`);
|
|
79
|
+
process.stdout.write(`${e.timestamp} ${e.level.padEnd(5)} ${e.context && e.context !== e.message ? `${e.context} ` : ""}${e.message}\n`);
|
|
80
80
|
}
|
|
81
81
|
export async function auditExport(opts) {
|
|
82
82
|
const org = orgSlug(opts);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** `nerd delete`: the app goes offline now; the API keeps the data for a grace period and can restore it. */
|
|
2
|
+
export declare function deleteProject(opts: {
|
|
3
|
+
json: boolean;
|
|
4
|
+
yes: boolean;
|
|
5
|
+
org?: string | undefined;
|
|
6
|
+
slug?: string | undefined;
|
|
7
|
+
}): Promise<void>;
|
|
8
|
+
/** `nerd trash`: deleted projects still inside their grace period. */
|
|
9
|
+
export declare function trashList(opts: {
|
|
10
|
+
json: boolean;
|
|
11
|
+
org?: string | undefined;
|
|
12
|
+
}): Promise<void>;
|
|
13
|
+
/** `nerd trash restore <slug>`: undo a delete. The app stays offline until the next deploy or rollback. */
|
|
14
|
+
export declare function trashRestore(slug: string | undefined, opts: {
|
|
15
|
+
json: boolean;
|
|
16
|
+
org?: string | undefined;
|
|
17
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { ApiClient } from "../api.js";
|
|
2
|
+
import { findProjectRoot, readNerdJson } from "../project.js";
|
|
3
|
+
import { CliError, log, printJson, table } from "../util.js";
|
|
4
|
+
/** The project named on the command line, or the one this directory is linked to. */
|
|
5
|
+
async function target(opts) {
|
|
6
|
+
if (opts.slug) {
|
|
7
|
+
if (opts.org)
|
|
8
|
+
return { org: opts.org, project: opts.slug };
|
|
9
|
+
try {
|
|
10
|
+
return { org: readNerdJson(findProjectRoot()).org, project: opts.slug };
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
const me = await ApiClient.fromConfig().get("/v1/me");
|
|
14
|
+
if (me.orgs.length === 1)
|
|
15
|
+
return { org: me.orgs[0].slug, project: opts.slug };
|
|
16
|
+
throw new CliError(`Pass --org too. Yours: ${me.orgs.map((o) => o.slug).join(", ")}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const nerd = readNerdJson(findProjectRoot());
|
|
20
|
+
return { org: opts.org ?? nerd.org, project: nerd.project };
|
|
21
|
+
}
|
|
22
|
+
async function confirm(question) {
|
|
23
|
+
if (!process.stdin.isTTY || process.env.CI)
|
|
24
|
+
return false;
|
|
25
|
+
const { createInterface } = await import("node:readline/promises");
|
|
26
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
27
|
+
try {
|
|
28
|
+
const answer = await rl.question(`${question} [y/N] `);
|
|
29
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
rl.close();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** `nerd delete`: the app goes offline now; the API keeps the data for a grace period and can restore it. */
|
|
36
|
+
export async function deleteProject(opts) {
|
|
37
|
+
const t = await target(opts);
|
|
38
|
+
const name = `${t.org}/${t.project}`;
|
|
39
|
+
if (!opts.yes) {
|
|
40
|
+
if (opts.json)
|
|
41
|
+
throw new CliError(`Refusing to delete ${name} without --yes.`);
|
|
42
|
+
const ok = await confirm(`Delete ${name}? The app goes offline now; its database and files are kept for a week and can be restored with \`nerd trash restore ${t.project}\`.`);
|
|
43
|
+
if (!ok)
|
|
44
|
+
throw new CliError(`Not deleted. Pass --yes to skip the question.`);
|
|
45
|
+
}
|
|
46
|
+
const r = await ApiClient.fromConfig().delete(`/v1/orgs/${t.org}/projects/${t.project}`);
|
|
47
|
+
if (opts.json)
|
|
48
|
+
return printJson({ ...r, org: t.org, project: t.project });
|
|
49
|
+
log.ok(`Deleted ${name}. The app is offline; its data is destroyed after ${r.purgeAfter.slice(0, 10)}.`);
|
|
50
|
+
log.info(`Changed your mind? \`nerd trash restore ${t.project}${opts.org ? ` --org ${t.org}` : ""}\` before then (API: ${r.restore}).`);
|
|
51
|
+
}
|
|
52
|
+
/** `nerd trash`: deleted projects still inside their grace period. */
|
|
53
|
+
export async function trashList(opts) {
|
|
54
|
+
let org = opts.org;
|
|
55
|
+
if (!org) {
|
|
56
|
+
try {
|
|
57
|
+
org = readNerdJson(findProjectRoot()).org;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
const me = await ApiClient.fromConfig().get("/v1/me");
|
|
61
|
+
if (me.orgs.length !== 1)
|
|
62
|
+
throw new CliError(`Pass --org. Yours: ${me.orgs.map((o) => o.slug).join(", ")}`);
|
|
63
|
+
org = me.orgs[0].slug;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const r = await ApiClient.fromConfig().get(`/v1/orgs/${org}/trash`);
|
|
67
|
+
if (opts.json)
|
|
68
|
+
return printJson(r);
|
|
69
|
+
if (!r.projects.length)
|
|
70
|
+
return log.info(`Nothing in the trash for ${org}.`);
|
|
71
|
+
table(r.projects.map((p) => ({ slug: p.slug, name: p.name, purgeAfter: p.purgeAfter.slice(0, 10) })), ["slug", "name", "purgeAfter"]);
|
|
72
|
+
log.info(`Restore one with: nerd trash restore <slug>${opts.org ? ` --org ${org}` : ""}`);
|
|
73
|
+
}
|
|
74
|
+
/** `nerd trash restore <slug>`: undo a delete. The app stays offline until the next deploy or rollback. */
|
|
75
|
+
export async function trashRestore(slug, opts) {
|
|
76
|
+
if (!slug)
|
|
77
|
+
throw new CliError("Usage: nerd trash restore <slug> [--org <org>]");
|
|
78
|
+
const t = await target({ org: opts.org, slug });
|
|
79
|
+
const r = await ApiClient.fromConfig().post(`/v1/orgs/${t.org}/trash/${t.project}`, {});
|
|
80
|
+
if (opts.json)
|
|
81
|
+
return printJson(r);
|
|
82
|
+
log.ok(`Restored ${t.org}/${r.project.slug}.`);
|
|
83
|
+
log.info(r.next);
|
|
84
|
+
}
|
package/dist/commands/verify.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { ApiClient } from "../api.js";
|
|
4
|
-
import { BUILD_GATES, knownSecretsFrom, runGates, SOURCE_GATES } from "../gates/index.js";
|
|
4
|
+
import { BUILD_GATES, gateSummary, knownSecretsFrom, runGates, skippedGates, SOURCE_GATES } from "../gates/index.js";
|
|
5
5
|
import { findProjectRoot, projectPath, readDevVars, readNerdJson } from "../project.js";
|
|
6
6
|
import { CliError, log } from "../util.js";
|
|
7
7
|
import { readBuildOutput } from "./deploy.js";
|
|
@@ -39,9 +39,9 @@ export async function verify(opts) {
|
|
|
39
39
|
const root = findProjectRoot();
|
|
40
40
|
const out = await runVerify(root, { withBuild: !opts["no-build"], stopOnFail: false, quiet: opts.json, onlyRules: opts.rule ? [opts.rule] : undefined });
|
|
41
41
|
if (opts.json)
|
|
42
|
-
process.stdout.write(`${JSON.stringify({ ok: out.ok, failures: out.failures, stop: out.stop }, null, 2)}\n`);
|
|
42
|
+
process.stdout.write(`${JSON.stringify({ ok: out.ok, failures: out.failures, stop: out.stop, skipped: skippedGates(out.results) }, null, 2)}\n`);
|
|
43
43
|
else if (out.ok)
|
|
44
|
-
log.ok(
|
|
44
|
+
log.ok(gateSummary(out.results));
|
|
45
45
|
const human = out.failures.filter((f) => f.audience === "human" && f.severity === "block");
|
|
46
46
|
if (!opts.json && human.length)
|
|
47
47
|
log.warn(`${human.length} finding(s) need a person, not the agent: ${human.map((f) => f.rule_id).join(", ")}`);
|
package/dist/gates/code.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type AdvisorySeverity } from "./text.js";
|
|
1
2
|
import { type Gate } from "./types.js";
|
|
2
3
|
export declare function exec(cmd: string, args: string[], cwd: string): Promise<{
|
|
3
4
|
code: number;
|
|
@@ -8,7 +9,9 @@ export declare function hasTool(name: string): Promise<boolean>;
|
|
|
8
9
|
/** Platform rules, as `nerd check` enforces them. The rule text is ours, so it is safe to show verbatim. */
|
|
9
10
|
export declare const rulesGate: Gate;
|
|
10
11
|
export declare const typecheckGate: Gate;
|
|
11
|
-
/**
|
|
12
|
+
/** The catalog rule for an advisory severity. Critical and high block, as the homepage promises; moderate warns; low and unrated inform. */
|
|
13
|
+
export declare function depsRuleFor(severity: AdvisorySeverity): string;
|
|
14
|
+
/** Known vulnerabilities in dependencies. Critical and high block; moderate warns. Package names come from the registry, not the repo. */
|
|
12
15
|
export declare const depsGate: Gate;
|
|
13
16
|
export declare const policyGate: Gate;
|
|
14
17
|
/** Heuristics for careless handling of personal data. Advisory: warns, never blocks. */
|
package/dist/gates/code.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { staticChecks } from "../commands/check.js";
|
|
5
5
|
import { lineOf, sourceFiles } from "./fs.js";
|
|
6
6
|
import { finding } from "./secrets.js";
|
|
7
|
+
import { BLOCKED_PACKAGES, MINING_POOLS, normalizeSeverity, osvSeverity } from "./text.js";
|
|
7
8
|
import { counts, result, verdict } from "./types.js";
|
|
8
9
|
export function exec(cmd, args, cwd) {
|
|
9
10
|
return new Promise((resolve) => {
|
|
@@ -66,7 +67,11 @@ function depsVerdictSummary(findings) {
|
|
|
66
67
|
return "No lockfile; commit one so installs are reproducible";
|
|
67
68
|
return findings.length ? counts(findings) : "No known vulnerabilities";
|
|
68
69
|
}
|
|
69
|
-
/**
|
|
70
|
+
/** The catalog rule for an advisory severity. Critical and high block, as the homepage promises; moderate warns; low and unrated inform. */
|
|
71
|
+
export function depsRuleFor(severity) {
|
|
72
|
+
return severity === "critical" ? "deps/critical-vulnerability" : severity === "high" ? "deps/high-vulnerability" : severity === "moderate" ? "deps/moderate-vulnerability" : severity === "low" ? "deps/low-vulnerability" : "deps/advisory";
|
|
73
|
+
}
|
|
74
|
+
/** Known vulnerabilities in dependencies. Critical and high block; moderate warns. Package names come from the registry, not the repo. */
|
|
70
75
|
export const depsGate = {
|
|
71
76
|
name: "dependencies",
|
|
72
77
|
title: "Dependency vulnerabilities",
|
|
@@ -78,7 +83,6 @@ export const depsGate = {
|
|
|
78
83
|
const lock = ["pnpm-lock.yaml", "package-lock.json", "yarn.lock"].find((f) => existsSync(path.join(ctx.root, f)));
|
|
79
84
|
if (!lock)
|
|
80
85
|
findings.push(finding("deps/lockfile-required", {}));
|
|
81
|
-
const sev = (s) => (s.toUpperCase() === "CRITICAL" ? "deps/critical-vulnerability" : s.toUpperCase() === "HIGH" ? "deps/high-vulnerability" : "deps/advisory");
|
|
82
86
|
if (await hasTool("osv-scanner")) {
|
|
83
87
|
const r = await exec("osv-scanner", ["scan", "source", "--format", "json", "--lockfile", lock ?? "pnpm-lock.yaml"], ctx.root);
|
|
84
88
|
try {
|
|
@@ -86,7 +90,7 @@ export const depsGate = {
|
|
|
86
90
|
for (const res of json.results ?? [])
|
|
87
91
|
for (const p of res.packages ?? [])
|
|
88
92
|
for (const v of p.vulnerabilities) {
|
|
89
|
-
const id =
|
|
93
|
+
const id = depsRuleFor(osvSeverity(v));
|
|
90
94
|
findings.push(finding(id, { file: lock, fix: `Update ${p.package.name} from ${p.package.version} to a version without ${v.id} (https://osv.dev/vulnerability/${v.id}); \`pnpm update ${p.package.name}\`.`, fingerprint: `${v.id}:${p.package.name}` }));
|
|
91
95
|
}
|
|
92
96
|
return result(this, verdict(findings), depsVerdictSummary(findings), findings, t, "osv-scanner");
|
|
@@ -100,7 +104,7 @@ export const depsGate = {
|
|
|
100
104
|
try {
|
|
101
105
|
const json = JSON.parse(r.stdout);
|
|
102
106
|
for (const a of Object.values(json.advisories ?? {})) {
|
|
103
|
-
findings.push(finding(
|
|
107
|
+
findings.push(finding(depsRuleFor(normalizeSeverity(a.severity)), { file: lock, fix: `Update ${a.module_name} to a fixed version${a.url ? ` (${a.url})` : ""}; \`${pm} update ${a.module_name}\`.`, fingerprint: `${a.url ?? a.module_name}` }));
|
|
104
108
|
}
|
|
105
109
|
}
|
|
106
110
|
catch {
|
|
@@ -111,7 +115,6 @@ export const depsGate = {
|
|
|
111
115
|
},
|
|
112
116
|
};
|
|
113
117
|
/** Packages that have no place in a hosted web app, and size caps. Deterministic policy only. */
|
|
114
|
-
import { BLOCKED_PACKAGES, MINING_POOLS } from "./text.js";
|
|
115
118
|
const MAX_WORKER_BYTES = 10 * 1024 * 1024;
|
|
116
119
|
const MAX_ASSET_BYTES = 25 * 1024 * 1024;
|
|
117
120
|
export const policyGate = {
|
package/dist/gates/fs.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
const SKIP_DIRS = new Set(["node_modules", ".git", "dist", ".wrangler", ".vite", "coverage"]);
|
|
4
|
-
const TEXT_EXT = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json", ".jsonc", ".md", ".mdc", ".yml", ".yaml", ".toml", ".env", ".txt", ".html", ".css", ".sql", ".example"]);
|
|
4
|
+
const TEXT_EXT = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json", ".jsonc", ".md", ".mdc", ".yml", ".yaml", ".toml", ".env", ".txt", ".html", ".css", ".sql", ".example", ".sample", ".template"]);
|
|
5
5
|
const MAX_BYTES = 2 * 1024 * 1024;
|
|
6
6
|
/** Text files under root, skipping dependencies and build output. */
|
|
7
7
|
export function sourceFiles(root) {
|
package/dist/gates/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Finding, Gate, GateContext, GateResult } from "./types.js";
|
|
2
|
-
export type { Audience, Finding, Gate, GateContext, GateResult, GateStatus, Severity } from "./types.js";
|
|
2
|
+
export type { Audience, Finding, Gate, GateContext, GateResult, GateStatus, Severity, SkippedGate } from "./types.js";
|
|
3
|
+
export { gateSummary, skippedGates } from "./types.js";
|
|
3
4
|
export { RULES, ruleFor, type RuleDef } from "./rules.js";
|
|
4
5
|
export { mask } from "./secrets.js";
|
|
5
6
|
export { MAX_ATTEMPTS, readAttempts } from "./attempts.js";
|
package/dist/gates/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { depsGate, piiGate, policyGate, rulesGate, typecheckGate } from "./code.
|
|
|
3
3
|
import { gitleaksGate, semgrepGate } from "./tools.js";
|
|
4
4
|
import { suppressionsGate } from "./suppressions.js";
|
|
5
5
|
import { applyAttempts, MAX_ATTEMPTS } from "./attempts.js";
|
|
6
|
+
export { gateSummary, skippedGates } from "./types.js";
|
|
6
7
|
export { RULES, ruleFor } from "./rules.js";
|
|
7
8
|
export { mask } from "./secrets.js";
|
|
8
9
|
export { MAX_ATTEMPTS, readAttempts } from "./attempts.js";
|
package/dist/gates/rules.js
CHANGED
|
@@ -23,11 +23,14 @@ export const RULES = {
|
|
|
23
23
|
"secrets/rotate-committed-secret": { title: "A committed secret must be rotated", severity: "block", audience: "human", risk: "Removing a secret from the code does not remove it from git history; anyone who fetched the repository still has it.", fix: "Rotate the credential at the provider that issued it (create a new one, update the platform secret, revoke the old one). Then the agent can remove it from the code.", example: { wrong: "Delete the line and commit", right: "Rotate at the provider first, then delete the line" } },
|
|
24
24
|
"bundle/known-secret-in-client": { title: "A server secret is in the client bundle", severity: "block", audience: "agent", risk: "A value from .dev.vars is compiled into JavaScript that every visitor downloads.", fix: "Only VITE_-prefixed variables belong in the client, and none of them may be secrets. Move the call that needs the secret to a server route.", example: { wrong: "fetch(api, { headers: { Authorization: import.meta.env.VITE_API_SECRET } })", right: "app.post('/api/thing', async (c) => fetch(api, { headers: { Authorization: c.env.API_SECRET } }))" } },
|
|
25
25
|
"bundle/known-secret-in-server": { title: "A local secret value is in the server bundle", severity: "block", audience: "agent", risk: "A value that should come from platform secrets is hard-coded in the built server.", fix: "Read it from c.env instead of embedding it." },
|
|
26
|
+
"bundle/local-secrets-in-dist": { title: "Local secrets file copied into build output", severity: "warn", audience: "agent", risk: "A local secrets file (.dev.vars or .env*) was copied into dist/ by the build. It is not uploaded, but anyone who zips or shares dist/ ships the secrets with it.", fix: "Nothing to change in the code: do not zip or share dist/. Delete the copy with `rm dist/*/.dev.vars` before handing the folder to anyone, and keep .dev.vars and .env* in .gitignore." },
|
|
26
27
|
"bundle/vite-secret-exposed": { title: "A VITE_ variable looks like a secret", severity: "block", audience: "agent", risk: "VITE_ variables are public: this one is compiled into the client where anyone can read it.", fix: "Rename it without the VITE_ prefix, set it as a server secret, and use it only in server code.", example: { wrong: "VITE_STRIPE_SECRET_KEY=sk_live_...", right: "STRIPE_SECRET_KEY as a platform secret, read on the server" } },
|
|
27
28
|
"deps/lockfile-required": { title: "No lockfile", severity: "block", audience: "agent", risk: "Without a lockfile every install can pull different versions, so what passed checks is not what deploys.", fix: "Run `pnpm install` and commit pnpm-lock.yaml." },
|
|
28
|
-
"deps/critical-vulnerability": { title: "Dependency with a critical vulnerability", severity: "block", audience: "agent", risk: "A known, exploitable bug
|
|
29
|
-
"deps/high-vulnerability": { title: "Dependency with a high-severity
|
|
30
|
-
"deps/
|
|
29
|
+
"deps/critical-vulnerability": { title: "Dependency with a critical vulnerability", severity: "block", audience: "agent", risk: "A package your app ships has a critical, known, exploitable bug; attackers scan for exactly these.", fix: "Update the package to a fixed version (`pnpm update <name>`), or replace it if no fix exists. Dependabot pull requests do this automatically.", example: { wrong: '"lodash": "4.17.15"', right: '"lodash": "^4.17.21"' } },
|
|
30
|
+
"deps/high-vulnerability": { title: "Dependency with a high-severity vulnerability", severity: "block", audience: "agent", risk: "A package your app ships has a high-severity known bug that is exploitable in common setups.", fix: "Update the package to a fixed version (`pnpm update <name>`), or replace it if no fix exists.", example: { wrong: '"lodash": "4.17.15"', right: '"lodash": "^4.17.21"' } },
|
|
31
|
+
"deps/moderate-vulnerability": { title: "Dependency with a moderate-severity advisory", severity: "warn", audience: "agent", risk: "A package your app ships has a known bug of moderate severity; exploitable only in some setups, but still worth fixing before it grows.", fix: "Update the package when a fixed version exists (`pnpm update <name>`)." },
|
|
32
|
+
"deps/low-vulnerability": { title: "Dependency with a low-severity advisory", severity: "info", audience: "agent", risk: "A package your app ships has a known issue of low severity.", fix: "Update when convenient." },
|
|
33
|
+
"deps/advisory": { title: "Dependency advisory", severity: "info", audience: "agent", risk: "A package your app ships has a published advisory whose severity is not rated yet.", fix: "Read the advisory at the link in the finding and update if it applies to how you use the package." },
|
|
31
34
|
"deps/audit-unavailable": { title: "Dependency audit could not run", severity: "warn", audience: "human", risk: "Vulnerable packages would not be detected this run.", fix: "Check network access and the package manager, then run `nerd verify` again." },
|
|
32
35
|
"rules/platform": { title: "Platform rule", severity: "block", audience: "agent", risk: "The app would not run correctly on the platform.", fix: "Follow the instruction in the finding; the rules are listed in the nerdsystem skill." },
|
|
33
36
|
"rules/advisory": { title: "Platform advisory", severity: "warn", audience: "agent", risk: "Not blocking, but the project is missing something the platform expects.", fix: "Follow the instruction in the finding." },
|
package/dist/gates/secrets.d.ts
CHANGED
|
@@ -5,6 +5,11 @@ export declare function finding(ruleId: string, extra: Partial<Finding> & {
|
|
|
5
5
|
file?: string | undefined;
|
|
6
6
|
line?: number | undefined;
|
|
7
7
|
}): Finding;
|
|
8
|
+
/**
|
|
9
|
+
* Findings for every credential-shaped value in a text. A hit in an example file or with an obvious
|
|
10
|
+
* placeholder value is reported as a warning with the reason in its risk text, so it is seen but
|
|
11
|
+
* never blocks a deploy and never asks a person to rotate anything.
|
|
12
|
+
*/
|
|
8
13
|
export declare function scanText(text: string, file: string): Finding[];
|
|
9
14
|
export declare const secretsGate: Gate;
|
|
10
15
|
/** Built output must not carry server secrets or anything that looks like one. */
|
package/dist/gates/secrets.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { exec } from "./code.js";
|
|
5
5
|
import { sourceFiles } from "./fs.js";
|
|
6
|
-
import { findSecrets, mask } from "./text.js";
|
|
6
|
+
import { ENV_FILES, findSecrets, isExampleFile, mask } from "./text.js";
|
|
7
7
|
import { RULES } from "./rules.js";
|
|
8
8
|
import { counts, result, verdict } from "./types.js";
|
|
9
9
|
export { mask } from "./text.js";
|
|
@@ -26,8 +26,19 @@ export function finding(ruleId, extra) {
|
|
|
26
26
|
fingerprint: extra.fingerprint ?? fingerprint(ruleId, extra.file, extra.line),
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Findings for every credential-shaped value in a text. A hit in an example file or with an obvious
|
|
31
|
+
* placeholder value is reported as a warning with the reason in its risk text, so it is seen but
|
|
32
|
+
* never blocks a deploy and never asks a person to rotate anything.
|
|
33
|
+
*/
|
|
29
34
|
export function scanText(text, file) {
|
|
30
|
-
return findSecrets(text, file).map((h) =>
|
|
35
|
+
return findSecrets(text, file).map((h) => {
|
|
36
|
+
const f = finding(`secrets/${h.rule}`, { file: h.file, line: h.line, masked: h.masked });
|
|
37
|
+
if (!h.downgrade)
|
|
38
|
+
return f;
|
|
39
|
+
const note = h.downgrade === "example file" ? "This is an example file, so it is shown as a warning; keep only placeholder values in it." : "The value looks like a placeholder (default password, local host, or stand-in text), so this is a warning, not a block; replace it with an environment reference anyway.";
|
|
40
|
+
return { ...f, severity: f.severity === "block" ? "warn" : f.severity, risk: `${f.risk} (${h.downgrade}) ${note}` };
|
|
41
|
+
});
|
|
31
42
|
}
|
|
32
43
|
async function isTracked(root, file) {
|
|
33
44
|
if (!existsSync(path.join(root, ".git")))
|
|
@@ -44,28 +55,39 @@ export const secretsGate = {
|
|
|
44
55
|
const t = Date.now();
|
|
45
56
|
const findings = [];
|
|
46
57
|
for (const f of sourceFiles(ctx.root)) {
|
|
47
|
-
if (/(^|\/)\.env(\.|$)/.test(f.rel) && !
|
|
58
|
+
if (/(^|\/)\.env(\.|$)/.test(f.rel) && !isExampleFile(f.rel))
|
|
48
59
|
findings.push(finding("secrets/env-file-committed", { file: f.rel }));
|
|
49
60
|
if (/(^|\/)\.dev\.vars$/.test(f.rel))
|
|
50
61
|
findings.push(finding("secrets/dev-vars-committed", { file: f.rel }));
|
|
51
|
-
if (f.rel.endsWith(".example"))
|
|
52
|
-
continue;
|
|
53
62
|
findings.push(...scanText(f.text, f.rel));
|
|
54
63
|
}
|
|
55
64
|
const gitignore = path.join(ctx.root, ".gitignore");
|
|
56
65
|
if (!existsSync(gitignore) || !/\.dev\.vars/.test(readFileSync(gitignore, "utf8")))
|
|
57
66
|
findings.push(finding("secrets/gitignore-dev-vars", { file: ".gitignore" }));
|
|
58
67
|
// A secret in a committed file is already in history: removing it is not enough, a person must rotate it.
|
|
59
|
-
|
|
68
|
+
// Only a blocking finding counts: example files and placeholder values are warnings and need no rotation.
|
|
69
|
+
const credentialFiles = Array.from(new Set(findings.filter((f) => f.file && f.masked && f.severity === "block").map((f) => f.file)));
|
|
60
70
|
for (const file of credentialFiles) {
|
|
61
71
|
if (ctx.committed || (await isTracked(ctx.root, file))) {
|
|
62
|
-
const first = findings.find((f) => f.file === file && f.masked);
|
|
72
|
+
const first = findings.find((f) => f.file === file && f.masked && f.severity === "block");
|
|
63
73
|
findings.push(finding("secrets/rotate-committed-secret", { file, line: first.line, masked: first.masked }));
|
|
64
74
|
}
|
|
65
75
|
}
|
|
66
76
|
return result(this, verdict(findings), findings.length ? counts(findings) : "No credentials found in source", findings, t);
|
|
67
77
|
},
|
|
68
78
|
};
|
|
79
|
+
const MAX_SCAN_BYTES = 8 * 1024 * 1024;
|
|
80
|
+
function walkFiles(dir) {
|
|
81
|
+
const out = [];
|
|
82
|
+
for (const entry of readdirSync(dir)) {
|
|
83
|
+
const full = path.join(dir, entry);
|
|
84
|
+
if (statSync(full).isDirectory())
|
|
85
|
+
out.push(...walkFiles(full));
|
|
86
|
+
else
|
|
87
|
+
out.push(full);
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
69
91
|
/** Built output must not carry server secrets or anything that looks like one. */
|
|
70
92
|
export const bundleGate = {
|
|
71
93
|
name: "bundle",
|
|
@@ -86,12 +108,30 @@ export const bundleGate = {
|
|
|
86
108
|
if (isClient && /VITE_[A-Z0-9_]*(SECRET|PRIVATE|PASSWORD|TOKEN|API_KEY)[A-Z0-9_]*/.test(text))
|
|
87
109
|
findings.push(finding("bundle/vite-secret-exposed", { file: rel }));
|
|
88
110
|
};
|
|
111
|
+
// The whole dist/ tree, not only what is uploaded: the Vite plugin copies .dev.vars next to the
|
|
112
|
+
// Worker, and anything else that lands here travels with the folder when someone zips it.
|
|
113
|
+
const dist = path.join(ctx.root, "dist");
|
|
114
|
+
const assetsDir = ctx.build.assetsDir ? path.resolve(ctx.build.assetsDir) : undefined;
|
|
115
|
+
const seen = new Set();
|
|
116
|
+
for (const full of existsSync(dist) ? walkFiles(dist) : []) {
|
|
117
|
+
const rel = path.relative(ctx.root, full).split(path.sep).join("/");
|
|
118
|
+
seen.add(path.resolve(full));
|
|
119
|
+
if (ENV_FILES.test(rel)) {
|
|
120
|
+
findings.push(finding("bundle/local-secrets-in-dist", { file: rel }));
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (!/\.(js|mjs|html|css|json|map|txt)$/.test(rel) || statSync(full).size > MAX_SCAN_BYTES)
|
|
124
|
+
continue;
|
|
125
|
+
const isClient = assetsDir ? path.resolve(full).startsWith(`${assetsDir}${path.sep}`) : rel.startsWith("dist/client/");
|
|
126
|
+
scan(rel, readFileSync(full, "utf8"), isClient);
|
|
127
|
+
}
|
|
128
|
+
// Build output that lives outside dist/ (tests, custom layouts): scan what the upload will carry.
|
|
89
129
|
for (const m of ctx.build.modules)
|
|
90
|
-
if (/\.(js|mjs)$/.test(m))
|
|
130
|
+
if (/\.(js|mjs)$/.test(m) && !seen.has(path.resolve(ctx.build.workerDir, m)))
|
|
91
131
|
scan(`dist/worker/${m}`, readFileSync(path.join(ctx.build.workerDir, m), "utf8"), false);
|
|
92
132
|
if (ctx.build.assetsDir)
|
|
93
133
|
for (const a of ctx.build.assets)
|
|
94
|
-
if (/\.(js|mjs|html|css|json|map)$/.test(a))
|
|
134
|
+
if (/\.(js|mjs|html|css|json|map)$/.test(a) && !seen.has(path.resolve(ctx.build.assetsDir, a)))
|
|
95
135
|
scan(`dist/client/${a}`, readFileSync(path.join(ctx.build.assetsDir, a), "utf8"), true);
|
|
96
136
|
return result(this, verdict(findings), findings.length ? counts(findings) : "Built output is clean", findings, t);
|
|
97
137
|
},
|
package/dist/gates/text.d.ts
CHANGED
|
@@ -9,6 +9,11 @@ export declare const SECRET_PATTERNS: {
|
|
|
9
9
|
re: RegExp;
|
|
10
10
|
}[];
|
|
11
11
|
export declare function isAllowedLine(line: string): boolean;
|
|
12
|
+
/** Files that exist to show the shape of a secret, not to hold one. Findings in them warn instead of block. */
|
|
13
|
+
export declare const EXAMPLE_FILE: RegExp;
|
|
14
|
+
export declare function isExampleFile(file: string): boolean;
|
|
15
|
+
/** True when a matched credential is clearly a stand-in: a placeholder token, or a database URL with a default password or a local host. */
|
|
16
|
+
export declare function isPlaceholderValue(rule: string, value: string): boolean;
|
|
12
17
|
/** Type and last four characters only: "sk_live_****4f2a". Never the value. */
|
|
13
18
|
export declare function mask(match: string): string;
|
|
14
19
|
export declare function lineAt(text: string, index: number): number;
|
|
@@ -18,6 +23,12 @@ export interface SecretHit {
|
|
|
18
23
|
file: string;
|
|
19
24
|
line: number;
|
|
20
25
|
masked: string;
|
|
26
|
+
/**
|
|
27
|
+
* Why this hit should warn rather than block: the file is an example (`.env.example`, `*.sample`,
|
|
28
|
+
* `*.template`) or the value is an obvious stand-in (default password, local host, `xxx`, `your_`).
|
|
29
|
+
* Absent for a real-looking credential.
|
|
30
|
+
*/
|
|
31
|
+
downgrade?: "example file" | "placeholder value";
|
|
21
32
|
}
|
|
22
33
|
/** Every credential-shaped value in a text, one hit per line, masked. */
|
|
23
34
|
export declare function findSecrets(text: string, file: string, limit?: number): SecretHit[];
|
|
@@ -29,3 +40,31 @@ export declare const ENV_FILES: RegExp;
|
|
|
29
40
|
/** Source files worth scanning for text patterns; everything else is skipped as binary or generated. */
|
|
30
41
|
export declare const TEXT_FILE: RegExp;
|
|
31
42
|
export declare const SKIP_DIR: RegExp;
|
|
43
|
+
/** Advisory severity as the catalog names it. "unknown" when the database published none. */
|
|
44
|
+
export type AdvisorySeverity = "critical" | "high" | "moderate" | "low" | "unknown";
|
|
45
|
+
export declare function normalizeSeverity(raw: string | undefined | null): AdvisorySeverity;
|
|
46
|
+
/** The parts of an OSV vulnerability record that carry a severity. Matches osv-scanner's JSON and GET /v1/vulns/{id}. */
|
|
47
|
+
export interface OsvVulnerability {
|
|
48
|
+
id?: string;
|
|
49
|
+
database_specific?: {
|
|
50
|
+
severity?: string;
|
|
51
|
+
} | undefined;
|
|
52
|
+
severity?: {
|
|
53
|
+
type?: string;
|
|
54
|
+
score?: string;
|
|
55
|
+
}[] | undefined;
|
|
56
|
+
affected?: {
|
|
57
|
+
ecosystem_specific?: {
|
|
58
|
+
severity?: string;
|
|
59
|
+
};
|
|
60
|
+
}[] | undefined;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Severity of one OSV record: `database_specific.severity` (what GHSA publishes: CRITICAL, HIGH,
|
|
64
|
+
* MODERATE, LOW), else any `affected[].ecosystem_specific.severity`, else the CVSS 3.x base score
|
|
65
|
+
* computed from the vector in `severity[]`. The OSV batch query (`/v1/querybatch`) returns only ids;
|
|
66
|
+
* fetch `/v1/vulns/{id}` to get these fields.
|
|
67
|
+
*/
|
|
68
|
+
export declare function osvSeverity(v: OsvVulnerability): AdvisorySeverity;
|
|
69
|
+
/** CVSS v3.0/3.1 base score from a vector string, or null when the vector is malformed. */
|
|
70
|
+
export declare function cvss3BaseScore(vector: string): number | null;
|