@sphior/scan 0.1.1 → 0.1.3
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/cli.js +87 -12
- package/dist/server.cjs +2 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -17013,7 +17013,7 @@ function collectSourceFiles(rootDir, opts = {}) {
|
|
|
17013
17013
|
}
|
|
17014
17014
|
|
|
17015
17015
|
// src/engine/cli.ts
|
|
17016
|
-
import { writeFileSync } from "node:fs";
|
|
17016
|
+
import { existsSync, statSync as statSync2, writeFileSync } from "node:fs";
|
|
17017
17017
|
|
|
17018
17018
|
// src/engine/mcp/fix-context.ts
|
|
17019
17019
|
var DATA_BOUNDARY_NOTE = "SPHIOR does not transmit your source code. This payload contains only finding metadata and deterministic remediation guidance. Read the actual code from your local workspace.";
|
|
@@ -17094,16 +17094,58 @@ function buildFixContext(f) {
|
|
|
17094
17094
|
remediation_steps: remediationStepsFor(f),
|
|
17095
17095
|
references: f.references ?? [],
|
|
17096
17096
|
agent_instructions: agentInstructions(f),
|
|
17097
|
-
data_boundary_note: DATA_BOUNDARY_NOTE
|
|
17097
|
+
data_boundary_note: DATA_BOUNDARY_NOTE,
|
|
17098
|
+
also: []
|
|
17098
17099
|
};
|
|
17099
17100
|
}
|
|
17101
|
+
var SEV_ORDER = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
|
17100
17102
|
function buildFixContexts(findings) {
|
|
17101
|
-
const
|
|
17102
|
-
|
|
17103
|
+
const byLocation = /* @__PURE__ */ new Map();
|
|
17104
|
+
for (const f of findings) {
|
|
17105
|
+
const ctx = buildFixContext(f);
|
|
17106
|
+
const key = `${ctx.location.path}:${ctx.location.start_line}:${ctx.location.end_line}`;
|
|
17107
|
+
const bucket = byLocation.get(key);
|
|
17108
|
+
if (bucket) bucket.push(ctx);
|
|
17109
|
+
else byLocation.set(key, [ctx]);
|
|
17110
|
+
}
|
|
17111
|
+
const merged = [];
|
|
17112
|
+
for (const group of byLocation.values()) {
|
|
17113
|
+
group.sort((a, b) => SEV_ORDER[a.severity] - SEV_ORDER[b.severity]);
|
|
17114
|
+
const [primary, ...rest] = group;
|
|
17115
|
+
if (rest.length === 0) {
|
|
17116
|
+
merged.push(primary);
|
|
17117
|
+
continue;
|
|
17118
|
+
}
|
|
17119
|
+
const seen = new Set(primary.remediation_steps);
|
|
17120
|
+
const steps = [...primary.remediation_steps];
|
|
17121
|
+
for (const r of rest) {
|
|
17122
|
+
for (const step of r.remediation_steps) {
|
|
17123
|
+
if (seen.has(step)) continue;
|
|
17124
|
+
seen.add(step);
|
|
17125
|
+
steps.push(step);
|
|
17126
|
+
}
|
|
17127
|
+
}
|
|
17128
|
+
const alsoTitles = rest.map((r) => `${r.title}${r.cwe ? ` (${r.cwe})` : ""}`).join("; ");
|
|
17129
|
+
merged.push({
|
|
17130
|
+
...primary,
|
|
17131
|
+
// まとめた結果を指示文にも反映する。1 箇所に複数の検出がある事実を伝えないと、
|
|
17132
|
+
// エージェントは主の 1 件だけ直して残りを見落とす
|
|
17133
|
+
agent_instructions: `${primary.agent_instructions} This location has ${group.length} findings \u2014 your single fix must satisfy all of them, not just the first: ${alsoTitles}.`,
|
|
17134
|
+
remediation_steps: steps,
|
|
17135
|
+
references: [.../* @__PURE__ */ new Set([...primary.references, ...rest.flatMap((r) => r.references)])],
|
|
17136
|
+
also: rest.map((r) => ({
|
|
17137
|
+
rule_id: r.rule_id,
|
|
17138
|
+
severity: r.severity,
|
|
17139
|
+
cwe: r.cwe,
|
|
17140
|
+
title: r.title
|
|
17141
|
+
}))
|
|
17142
|
+
});
|
|
17143
|
+
}
|
|
17144
|
+
return merged.sort((a, b) => SEV_ORDER[a.severity] - SEV_ORDER[b.severity]);
|
|
17103
17145
|
}
|
|
17104
17146
|
|
|
17105
17147
|
// src/engine/cli.ts
|
|
17106
|
-
var
|
|
17148
|
+
var SEV_ORDER2 = { info: 0, low: 1, medium: 2, high: 3, critical: 4 };
|
|
17107
17149
|
function parseFlags(rest) {
|
|
17108
17150
|
const flags2 = {};
|
|
17109
17151
|
for (let i2 = 0; i2 < rest.length; i2++) {
|
|
@@ -17119,32 +17161,60 @@ function parseFlags(rest) {
|
|
|
17119
17161
|
function runCli(argv, ctx = {}) {
|
|
17120
17162
|
const log = ctx.log ?? (() => {
|
|
17121
17163
|
});
|
|
17164
|
+
const logErr = ctx.logErr ?? log;
|
|
17122
17165
|
const writeFile = ctx.writeFile ?? ((p, d) => writeFileSync(p, d));
|
|
17123
17166
|
const [cmd, dir, ...rest] = argv;
|
|
17124
17167
|
if (cmd !== "run" || !dir) {
|
|
17125
|
-
|
|
17168
|
+
logErr("usage: sphior-scan run <dir> [--sarif out.sarif] [--json out.json] [--fix-context out.json] [--fail-on high]");
|
|
17126
17169
|
return { exitCode: 2, error: "bad_args" };
|
|
17127
17170
|
}
|
|
17128
17171
|
const flags2 = parseFlags(rest);
|
|
17172
|
+
if (!existsSync(dir) || !statSync2(dir).isDirectory()) {
|
|
17173
|
+
logErr(`error: \u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${dir}`);
|
|
17174
|
+
logErr(" \u8D70\u67FB\u3057\u3066\u3044\u306A\u3044\u305F\u3081\u300C\u554F\u984C\u306A\u3057\u300D\u3068\u306F\u5831\u544A\u3057\u307E\u305B\u3093\u3002\u30D1\u30B9\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002");
|
|
17175
|
+
return { exitCode: 2, error: "dir_not_found" };
|
|
17176
|
+
}
|
|
17129
17177
|
const files = collectSourceFiles(dir);
|
|
17130
17178
|
const result = runScan({ files });
|
|
17131
17179
|
const sarifJson = canonicalJson(result.sarif);
|
|
17132
17180
|
if (flags2.sarif) writeFile(flags2.sarif, sarifJson);
|
|
17133
17181
|
if (flags2.json) writeFile(flags2.json, canonicalJson(result.findings));
|
|
17134
17182
|
if (flags2["fix-context"]) writeFile(flags2["fix-context"], canonicalJson(buildFixContexts(result.findings)));
|
|
17135
|
-
if (!flags2.sarif && !flags2.json && !flags2["fix-context"])
|
|
17183
|
+
if (!flags2.sarif && !flags2.json && !flags2["fix-context"]) {
|
|
17184
|
+
if (ctx.isTty) {
|
|
17185
|
+
const rows = [...result.findings].sort((a, b) => SEV_ORDER2[b.severity] - SEV_ORDER2[a.severity]).map((f) => ({
|
|
17186
|
+
sev: f.severity,
|
|
17187
|
+
loc: `${f.location.path}:${f.location.start_line}`,
|
|
17188
|
+
text: `${f.title}${f.cwe ? ` (${f.cwe})` : ""}`
|
|
17189
|
+
}));
|
|
17190
|
+
const locW = Math.max(0, ...rows.map((r) => r.loc.length));
|
|
17191
|
+
for (const r of rows) logErr(` ${r.sev.padEnd(8)} ${r.loc.padEnd(locW)} ${r.text}`);
|
|
17192
|
+
if (rows.length > 0) logErr("");
|
|
17193
|
+
} else {
|
|
17194
|
+
log(sarifJson);
|
|
17195
|
+
}
|
|
17196
|
+
}
|
|
17197
|
+
if (files.length === 0) {
|
|
17198
|
+
logErr(`warning: ${dir} \u306B\u8D70\u67FB\u5BFE\u8C61\u306E\u30D5\u30A1\u30A4\u30EB\u304C\u3042\u308A\u307E\u305B\u3093\u3067\u3057\u305F\uFF080 \u4EF6\u8D70\u67FB\uFF09\u3002`);
|
|
17199
|
+
}
|
|
17136
17200
|
const counts = result.findings.reduce((acc, f) => {
|
|
17137
17201
|
acc[f.severity] = (acc[f.severity] ?? 0) + 1;
|
|
17138
17202
|
return acc;
|
|
17139
17203
|
}, {});
|
|
17140
|
-
|
|
17204
|
+
logErr(
|
|
17141
17205
|
`SPHIOR CODE: ${result.findings.length} findings (critical=${counts.critical ?? 0} high=${counts.high ?? 0} medium=${counts.medium ?? 0} low=${counts.low ?? 0}) engine=${result.engine_version} ruleset=${result.ruleset_version}`
|
|
17142
17206
|
);
|
|
17207
|
+
if (ctx.isTty && !flags2.sarif && !flags2.json && !flags2["fix-context"]) {
|
|
17208
|
+
logErr("");
|
|
17209
|
+
logErr(" --fix-context fc.json AI \u30A8\u30FC\u30B8\u30A7\u30F3\u30C8\u306B\u6E21\u3057\u3066\u4FEE\u6B63\u3055\u305B\u308B");
|
|
17210
|
+
logErr(" --sarif out.sarif GitHub code scanning / CI \u30C0\u30C3\u30B7\u30E5\u30DC\u30FC\u30C9\u3078");
|
|
17211
|
+
logErr(" --fail-on high \u6307\u5B9A\u4EE5\u4E0A\u306E\u91CD\u5927\u5EA6\u304C\u3042\u308C\u3070 exit 1\uFF08CI \u7528\uFF09");
|
|
17212
|
+
}
|
|
17143
17213
|
let exitCode = 0;
|
|
17144
17214
|
const failOn = flags2["fail-on"];
|
|
17145
|
-
if (failOn &&
|
|
17146
|
-
const threshold =
|
|
17147
|
-
if (result.findings.some((f) =>
|
|
17215
|
+
if (failOn && SEV_ORDER2[failOn] !== void 0) {
|
|
17216
|
+
const threshold = SEV_ORDER2[failOn];
|
|
17217
|
+
if (result.findings.some((f) => SEV_ORDER2[f.severity] >= threshold)) exitCode = 1;
|
|
17148
17218
|
}
|
|
17149
17219
|
return { exitCode, result, sarifJson };
|
|
17150
17220
|
}
|
|
@@ -23959,7 +24029,12 @@ async function main() {
|
|
|
23959
24029
|
})));
|
|
23960
24030
|
const res = runCli(process.argv.slice(2), {
|
|
23961
24031
|
cwd: process.cwd(),
|
|
23962
|
-
log: (m) => console.log(m)
|
|
24032
|
+
log: (m) => console.log(m),
|
|
24033
|
+
// データ(SARIF)
|
|
24034
|
+
logErr: (m) => console.error(m),
|
|
24035
|
+
// 診断(サマリ・警告・ヒント)
|
|
24036
|
+
// 端末なら人が読む形、パイプ/リダイレクトなら SARIF(`| jq` を壊さない)
|
|
24037
|
+
isTty: Boolean(process.stdout.isTTY)
|
|
23963
24038
|
});
|
|
23964
24039
|
process.exit(res.exitCode);
|
|
23965
24040
|
}
|
package/dist/server.cjs
CHANGED
|
@@ -26098,7 +26098,8 @@ function buildFixContext(f) {
|
|
|
26098
26098
|
remediation_steps: remediationStepsFor(f),
|
|
26099
26099
|
references: f.references ?? [],
|
|
26100
26100
|
agent_instructions: agentInstructions(f),
|
|
26101
|
-
data_boundary_note: DATA_BOUNDARY_NOTE
|
|
26101
|
+
data_boundary_note: DATA_BOUNDARY_NOTE,
|
|
26102
|
+
also: []
|
|
26102
26103
|
};
|
|
26103
26104
|
}
|
|
26104
26105
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sphior/scan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Deterministic source scanner — hardcoded secrets and insecure patterns (CWE) with AST/taint analysis. No AI, no network, no source upload. CLI + LSP for editors and CI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|