@sphior/scan 0.1.2 → 0.1.4

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
@@ -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++) {
@@ -17128,8 +17170,8 @@ function runCli(argv, ctx = {}) {
17128
17170
  }
17129
17171
  const flags2 = parseFlags(rest);
17130
17172
  if (!existsSync(dir) || !statSync2(dir).isDirectory()) {
17131
- logErr(`error: \u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${dir}`);
17132
- 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");
17173
+ logErr(`error: directory not found: ${dir}`);
17174
+ logErr(" Nothing was scanned, so this is not a clean result. Check the path.");
17133
17175
  return { exitCode: 2, error: "dir_not_found" };
17134
17176
  }
17135
17177
  const files = collectSourceFiles(dir);
@@ -17140,7 +17182,7 @@ function runCli(argv, ctx = {}) {
17140
17182
  if (flags2["fix-context"]) writeFile(flags2["fix-context"], canonicalJson(buildFixContexts(result.findings)));
17141
17183
  if (!flags2.sarif && !flags2.json && !flags2["fix-context"]) {
17142
17184
  if (ctx.isTty) {
17143
- const rows = [...result.findings].sort((a, b) => SEV_ORDER[b.severity] - SEV_ORDER[a.severity]).map((f) => ({
17185
+ const rows = [...result.findings].sort((a, b) => SEV_ORDER2[b.severity] - SEV_ORDER2[a.severity]).map((f) => ({
17144
17186
  sev: f.severity,
17145
17187
  loc: `${f.location.path}:${f.location.start_line}`,
17146
17188
  text: `${f.title}${f.cwe ? ` (${f.cwe})` : ""}`
@@ -17153,7 +17195,7 @@ function runCli(argv, ctx = {}) {
17153
17195
  }
17154
17196
  }
17155
17197
  if (files.length === 0) {
17156
- 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`);
17198
+ logErr(`warning: no scannable files found in ${dir} (0 files scanned).`);
17157
17199
  }
17158
17200
  const counts = result.findings.reduce((acc, f) => {
17159
17201
  acc[f.severity] = (acc[f.severity] ?? 0) + 1;
@@ -17164,15 +17206,15 @@ function runCli(argv, ctx = {}) {
17164
17206
  );
17165
17207
  if (ctx.isTty && !flags2.sarif && !flags2.json && !flags2["fix-context"]) {
17166
17208
  logErr("");
17167
- logErr(" --fix-context fc.json AI \u30A8\u30FC\u30B8\u30A7\u30F3\u30C8\u306B\u6E21\u3057\u3066\u4FEE\u6B63\u3055\u305B\u308B");
17168
- logErr(" --sarif out.sarif GitHub code scanning / CI \u30C0\u30C3\u30B7\u30E5\u30DC\u30FC\u30C9\u3078");
17169
- logErr(" --fail-on high \u6307\u5B9A\u4EE5\u4E0A\u306E\u91CD\u5927\u5EA6\u304C\u3042\u308C\u3070 exit 1\uFF08CI \u7528\uFF09");
17209
+ logErr(" --fix-context fc.json hand findings to an AI agent to fix");
17210
+ logErr(" --sarif out.sarif for GitHub code scanning / CI dashboards");
17211
+ logErr(" --fail-on high exit 1 if anything at or above this severity (CI)");
17170
17212
  }
17171
17213
  let exitCode = 0;
17172
17214
  const failOn = flags2["fail-on"];
17173
- if (failOn && SEV_ORDER[failOn] !== void 0) {
17174
- const threshold = SEV_ORDER[failOn];
17175
- 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;
17176
17218
  }
17177
17219
  return { exitCode, result, sarifJson };
17178
17220
  }
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.2",
3
+ "version": "0.1.4",
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": {