nerdsystem 0.0.18 → 0.0.19
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/commands/deploy.js
CHANGED
|
@@ -8,6 +8,7 @@ import { CliError, log, printJson, runOrThrow, table, dashboardUrl, ensureDeps }
|
|
|
8
8
|
import { runChecks } from "./check.js";
|
|
9
9
|
import { appendLineage, cliVersion, LOCAL_ONLY_NOTE, packProject, readLineage, uploadSnapshot, uploadStopMessage } from "./source.js";
|
|
10
10
|
import { lockedGetPaths } from "../gates/tests.js";
|
|
11
|
+
import { platformScan } from "./platform-scan.js";
|
|
11
12
|
import { BUILD_GATES, gateSummary, knownSecretsFrom, probeDeployment, runGates, skippedProbe, SOURCE_GATES, waitForDeployment } from "../gates/index.js";
|
|
12
13
|
const MODULE_EXT = new Set([".js", ".mjs", ".wasm"]);
|
|
13
14
|
function walk(dir, base = dir) {
|
|
@@ -155,6 +156,8 @@ export async function shipBuild(args) {
|
|
|
155
156
|
appendLineage(root, source.id);
|
|
156
157
|
// "Stored", not "saved": the one line that says "saved" is the final one, which also says the work after it is only on this computer (invariant 7).
|
|
157
158
|
log.ok(`Stored ${pack.fileCount} files (${formatMb(pack.bytes)}) as ${source.id}`);
|
|
159
|
+
// The scanners this computer lacks run on the platform against the copy just stored, before migrations or upload.
|
|
160
|
+
args.results = await platformScan(api, `${projectPath(nerd)}/sources/${source.id}`, args.results);
|
|
158
161
|
}
|
|
159
162
|
log.step("Applying migrations to the production database");
|
|
160
163
|
const mig = await applyRemoteMigrations(root, api);
|
package/dist/commands/init.js
CHANGED
|
@@ -192,4 +192,4 @@ export async function init(dir, args) {
|
|
|
192
192
|
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}`);
|
|
193
193
|
}
|
|
194
194
|
// Kept in sync with packages/sdk/package.json by the release script.
|
|
195
|
-
const SDK_VERSION = "0.0.
|
|
195
|
+
const SDK_VERSION = "0.0.19";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ApiClient } from "../api.js";
|
|
2
|
+
import type { GateResult } from "../gates/index.js";
|
|
3
|
+
export interface PlatformScanOptions {
|
|
4
|
+
/** How long the deploy waits for the answer. */
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
pollMs?: number;
|
|
7
|
+
sleep?: (ms: number) => Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* When gitleaks or semgrep did not run here because they are not installed, has the platform run them
|
|
11
|
+
* on the copy of the code that was just stored, and waits for the answer before anything goes live.
|
|
12
|
+
* Returns the gate results with the platform's answers in place of the skipped ones.
|
|
13
|
+
*
|
|
14
|
+
* A blocking finding stops the deploy, exactly as it would have locally. Anything else that goes wrong
|
|
15
|
+
* (the scanners are busy or down, the hourly allowance is used, the answer is late) does not: the two
|
|
16
|
+
* results stay "skip" with the reason, because a platform hiccup must not be what keeps an app offline.
|
|
17
|
+
*/
|
|
18
|
+
export declare function platformScan(api: ApiClient, sourcePath: string, results: GateResult[], opts?: PlatformScanOptions): Promise<GateResult[]>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { CliError, log } from "../util.js";
|
|
2
|
+
/** The two scanners most computers do not have. The platform's builder has both. */
|
|
3
|
+
const SCANNERS = ["gitleaks", "semgrep"];
|
|
4
|
+
const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
5
|
+
/**
|
|
6
|
+
* When gitleaks or semgrep did not run here because they are not installed, has the platform run them
|
|
7
|
+
* on the copy of the code that was just stored, and waits for the answer before anything goes live.
|
|
8
|
+
* Returns the gate results with the platform's answers in place of the skipped ones.
|
|
9
|
+
*
|
|
10
|
+
* A blocking finding stops the deploy, exactly as it would have locally. Anything else that goes wrong
|
|
11
|
+
* (the scanners are busy or down, the hourly allowance is used, the answer is late) does not: the two
|
|
12
|
+
* results stay "skip" with the reason, because a platform hiccup must not be what keeps an app offline.
|
|
13
|
+
*/
|
|
14
|
+
export async function platformScan(api, sourcePath, results, opts = {}) {
|
|
15
|
+
const missing = results.filter((r) => SCANNERS.includes(r.name) && r.status === "skip");
|
|
16
|
+
if (!missing.length)
|
|
17
|
+
return results;
|
|
18
|
+
const sleep = opts.sleep ?? defaultSleep;
|
|
19
|
+
const pollMs = opts.pollMs ?? 2500;
|
|
20
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 150_000);
|
|
21
|
+
const skipped = (reason) => results.map((r) => (missing.includes(r) ? { ...r, summary: `${r.name} is not installed here, and the platform's scan ${reason}` } : r));
|
|
22
|
+
log.step(`Scanning the stored code on the platform (${missing.map((m) => m.name).join(" and ")} are not installed here)`);
|
|
23
|
+
let scan;
|
|
24
|
+
try {
|
|
25
|
+
scan = (await api.post(`${sourcePath}/scan`)).scan;
|
|
26
|
+
while ((scan.status === "queued" || scan.status === "running") && Date.now() < deadline) {
|
|
27
|
+
await sleep(pollMs);
|
|
28
|
+
const next = (await api.get(`${sourcePath}/scan`)).scan;
|
|
29
|
+
if (next)
|
|
30
|
+
scan = next;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
const why = err instanceof CliError && err.status === 429 ? "was not run: this organization has used its scans for the hour" : "could not be started";
|
|
35
|
+
log.warn(`The platform's scan ${why}. Continuing with the checks that ran here.`);
|
|
36
|
+
return skipped(why);
|
|
37
|
+
}
|
|
38
|
+
if (scan.status !== "done" || !scan.results) {
|
|
39
|
+
const why = scan.status === "failed" ? "failed" : "did not answer in time";
|
|
40
|
+
log.warn(`The platform's scan ${why}. Continuing with the checks that ran here.`);
|
|
41
|
+
return skipped(why);
|
|
42
|
+
}
|
|
43
|
+
const answered = new Map(scan.results.filter((r) => SCANNERS.includes(r.name)).map((r) => [r.name, r]));
|
|
44
|
+
const merged = results.map((r) => (missing.includes(r) && answered.has(r.name) ? { ...answered.get(r.name), summary: `${answered.get(r.name).summary} (scanned on the platform)` } : r));
|
|
45
|
+
const ran = merged.filter((r) => missing.some((m) => m.name === r.name));
|
|
46
|
+
for (const r of ran) {
|
|
47
|
+
log.info(` ${r.status === "pass" ? "✓" : r.status === "fail" ? "✗" : r.status === "warn" ? "!" : "–"} ${r.title}: ${r.summary}`);
|
|
48
|
+
for (const f of r.findings.slice(0, 20)) {
|
|
49
|
+
log.info(` ${f.severity === "block" ? "✗" : "!"} ${f.rule_id}${f.file ? ` ${f.file}${f.line ? `:${f.line}` : ""}` : ""}${f.masked ? ` (${f.masked})` : ""}`);
|
|
50
|
+
log.info(` ${f.risk}`);
|
|
51
|
+
log.info(` Fix: ${f.fix}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const blocked = ran.filter((r) => r.status === "fail");
|
|
55
|
+
if (blocked.length)
|
|
56
|
+
throw new CliError(`Blocked by: ${blocked.map((g) => g.title).join(", ")}. Nothing went live. Fix the findings above, then deploy again.`, 2, { code: "gate_failed" });
|
|
57
|
+
return merged;
|
|
58
|
+
}
|
package/dist/gates/tools.js
CHANGED
|
@@ -53,6 +53,10 @@ export const semgrepGate = {
|
|
|
53
53
|
const findings = [];
|
|
54
54
|
try {
|
|
55
55
|
const json = JSON.parse(r.stdout);
|
|
56
|
+
// Semgrep exits 0 and reports nothing when its rules file does not load, which reads exactly like a clean
|
|
57
|
+
// scan. That hid a broken rules file from 2026-11-02 to 2026-11-10: a run that could not use the rules is not a pass.
|
|
58
|
+
if ((json.errors ?? []).some((e) => e.level === "error" && /SemgrepError|InvalidRule|config/i.test(e.type ?? "")))
|
|
59
|
+
return result(this, "warn", "semgrep could not load its rules", [], t, "semgrep");
|
|
56
60
|
for (const x of json.results.slice(0, 100)) {
|
|
57
61
|
const short = x.check_id.split(".").pop() ?? x.check_id;
|
|
58
62
|
const rule_id = `semgrep/${short}`;
|
package/package.json
CHANGED
package/rules/semgrep.yml
CHANGED
|
@@ -22,8 +22,9 @@ rules:
|
|
|
22
22
|
severity: WARNING
|
|
23
23
|
message: dangerouslySetInnerHTML with a non-literal value can inject scripts. Sanitize or avoid.
|
|
24
24
|
patterns:
|
|
25
|
-
|
|
26
|
-
- pattern
|
|
25
|
+
# Quoted: an unquoted "__html: $X" reads as a YAML mapping and makes the whole file invalid.
|
|
26
|
+
- pattern: '<$EL dangerouslySetInnerHTML={{ __html: $X }} />'
|
|
27
|
+
- pattern-not: '<$EL dangerouslySetInnerHTML={{ __html: "..." }} />'
|
|
27
28
|
- id: nerd-innerhtml-assignment
|
|
28
29
|
languages: [typescript, javascript]
|
|
29
30
|
severity: WARNING
|