@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 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 order = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
17102
- return findings.map(buildFixContext).sort((a, b) => order[a.severity] - order[b.severity]);
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 SEV_ORDER = { info: 0, low: 1, medium: 2, high: 3, critical: 4 };
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
- log("usage: sphior-scan run <dir> [--sarif out.sarif] [--json out.json] [--fix-context out.json] [--fail-on high]");
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"]) log(sarifJson);
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
- log(
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 && SEV_ORDER[failOn] !== void 0) {
17146
- const threshold = SEV_ORDER[failOn];
17147
- if (result.findings.some((f) => SEV_ORDER[f.severity] >= threshold)) exitCode = 1;
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.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": {