@sphior/scan 0.1.6 → 0.1.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 CHANGED
@@ -31,20 +31,44 @@ sphior-scan run <dir> [options]
31
31
 
32
32
  Options:
33
33
  --sarif <file> write SARIF 2.1.0 (GitHub code scanning, most CI tools)
34
- --json <file> write raw findings as JSON
35
- --fix-context <file> write finding context for an AI assistant to act on
36
- --fail-on <severity> exit non-zero at or above this severity
37
- (critical | high | medium | low)
34
+ --json <file> write the findings as JSON
35
+ --fix-context <file> write one entry per place to fix, for an AI assistant to act on
36
+ --suppressed <file> write the findings accepted via .sphiorignore or inline comments
37
+ --fail-on <severity> exit 1 at or above this severity
38
+ (critical | high | medium | low | info)
39
+ --no-ignore-file do not read .sphiorignore
38
40
  ```
39
41
 
40
- SARIF is written to stdout as well, so it pipes cleanly:
42
+ With none of `--sarif`, `--json` or `--fix-context`, results go to stdout: a table in a
43
+ terminal, SARIF when piped. A one-line summary (with engine and ruleset versions) always goes
44
+ to stderr.
41
45
 
42
46
  ```bash
43
47
  sphior-scan run ./src --fail-on high
44
48
  ```
45
49
 
46
- Exit code is `0` when nothing at or above `--fail-on` was found, non-zero otherwise —
47
- which is what you want in CI.
50
+ Exit codes: `0` when nothing reached `--fail-on` (or no threshold was given), `1` when something
51
+ did, `2` when the arguments are wrong or the directory does not exist — so a typo in CI never
52
+ passes as a clean scan.
53
+
54
+ ### Excluding paths and accepting findings
55
+
56
+ `.sphiorignore` at the scanned root, one entry per line — `<glob> [rule_id] [# reason]`:
57
+
58
+ ```gitignore
59
+ generated/** # no rule id: the files are not read
60
+ test/fixtures/** secret.* # rule id: scanned, then accepted with a reason
61
+ ```
62
+
63
+ Inline, with any line-comment syntax:
64
+
65
+ ```js
66
+ eval(template); // sphior-ignore: constant input from build time
67
+ // sphior-ignore-next-line secret.private_key: documentation string
68
+ ```
69
+
70
+ Accepted findings are left out of SARIF (GitHub code scanning ignores SARIF suppressions) and
71
+ written to `--suppressed` with their reason.
48
72
 
49
73
  ### GitHub Actions
50
74
 
@@ -89,7 +113,8 @@ upload, no telemetry, no language model, no account. It runs correctly with netw
89
113
  ## Links
90
114
 
91
115
  - Product: <https://sphior.com/code>
92
- - Issues: <https://github.com/Toraastrta/Sphior/issues>
116
+ - Source and full documentation: <https://github.com/Toraastrta/sphior-code>
117
+ - Issues: <https://github.com/Toraastrta/sphior-code/issues>
93
118
 
94
119
  ## License
95
120
 
package/dist/cli.js CHANGED
@@ -15274,6 +15274,24 @@ var PATTERN_RULES = [
15274
15274
  exts: JS,
15275
15275
  pattern: /createHash\s*\(\s*['"`](?:md5|sha1)['"`]\s*\)/gi
15276
15276
  },
15277
+ {
15278
+ // 2026-10-23: 自己完結コーパス(measure-gate/sast-corpus.ts)が検出欠落として
15279
+ // 検出した。pattern.go.weak_cipher と pattern.java.weak_cipher は存在するのに
15280
+ // **JS/TS 版が無く**、同じ弱点が Go/Java では検出され Node では無検出だった。
15281
+ //
15282
+ // 0-FP の作り: アルゴリズム名を**引用符の直後に固定**する。これが無いと
15283
+ // "description" のような語が `des` に当たる。安全な算法(aes-256-gcm /
15284
+ // aes-256-cbc 等)に当たらないことはテストで固定している。
15285
+ id: "pattern.js.weak_cipher",
15286
+ cwe: "CWE-327",
15287
+ owasp: "A02:2021",
15288
+ severity: "medium",
15289
+ title: "Weak cipher algorithm",
15290
+ message: "DES/3DES, RC2/RC4, Blowfish and IDEA are broken; ECB mode leaks plaintext structure.",
15291
+ remediation: "Use aes-256-gcm (AEAD) with a random IV via crypto.createCipheriv().",
15292
+ exts: JS,
15293
+ pattern: /create(?:De)?Cipher(?:iv)?\s*\(\s*['"`](?:des(?:-[a-z0-9-]*)?|rc[24](?:-[a-z0-9-]*)?|bf(?:-[a-z0-9-]*)?|blowfish|idea(?:-[a-z0-9-]*)?|[a-z0-9-]*-ecb)['"`]/gi
15294
+ },
15277
15295
  // ── Python ──
15278
15296
  {
15279
15297
  id: "pattern.py.requests_verify_false",
@@ -16614,7 +16632,39 @@ function collectTainted(scope, seed) {
16614
16632
  }
16615
16633
  return tainted;
16616
16634
  }
16617
- function scanSinks(scope, tainted) {
16635
+ function isFsModule(spec) {
16636
+ if (typeof spec !== "string") return false;
16637
+ const m = spec.replace(/^node:/, "");
16638
+ return m === "fs" || m === "fs/promises";
16639
+ }
16640
+ function collectFsImportedNames(ast) {
16641
+ const names = /* @__PURE__ */ new Set();
16642
+ walk(ast, (n) => {
16643
+ if (n.type === "ImportDeclaration" && isFsModule(n.source?.value)) {
16644
+ for (const spec of n.specifiers ?? []) {
16645
+ if (spec.type !== "ImportSpecifier") continue;
16646
+ const imported = spec.imported?.name;
16647
+ const local = spec.local?.name;
16648
+ if (typeof imported === "string" && FS_METHODS.has(imported) && typeof local === "string") names.add(local);
16649
+ }
16650
+ return;
16651
+ }
16652
+ if (n.type === "VariableDeclarator") {
16653
+ const init3 = n.init;
16654
+ const id = n.id;
16655
+ if (!init3 || !id || id.type !== "ObjectPattern") return;
16656
+ if (calleeName(init3) !== "require") return;
16657
+ if (!isFsModule(init3.arguments?.[0]?.value)) return;
16658
+ for (const prop of id.properties ?? []) {
16659
+ const key = prop.key?.name;
16660
+ const val = prop.value?.name;
16661
+ if (typeof key === "string" && FS_METHODS.has(key) && typeof val === "string") names.add(val);
16662
+ }
16663
+ }
16664
+ });
16665
+ return names;
16666
+ }
16667
+ function scanSinks(scope, tainted, fsNames = /* @__PURE__ */ new Set()) {
16618
16668
  const hits = [];
16619
16669
  const hit = (ruleId, node) => {
16620
16670
  hits.push({ ruleId, node });
@@ -16633,6 +16683,7 @@ function scanSinks(scope, tainted) {
16633
16683
  else if (memberPath(callee) === "document.write" && exprIsTainted(arg0, tainted)) hit("taint.js.xss", n);
16634
16684
  else if ((idName === "fetch" || idName === "axios" || idName === "got") && exprIsTainted(arg0, tainted) || !!method && HTTP_METHODS.has(method) && !!objPath && HTTP_CLIENT_OBJECTS.has(objPath) && exprIsTainted(arg0, tainted)) hit("taint.js.ssrf", n);
16635
16685
  else if (!!method && FS_METHODS.has(method) && !!objPath && (objPath === "fs" || objPath === "fs.promises" || objPath === "fsp") && exprIsTainted(arg0, tainted)) hit("taint.js.path_traversal", n);
16686
+ else if (!!idName && fsNames.has(idName) && exprIsTainted(arg0, tainted)) hit("taint.js.path_traversal", n);
16636
16687
  else if (REDIRECT_CALLS.has(memberPath(callee) ?? "") && exprIsTainted(arg0, tainted)) hit("taint.js.open_redirect", n);
16637
16688
  else if (!!method && ["send", "write", "end"].includes(method) && !!objPath && (objPath === "res" || objPath === "response") && exprIsTainted(arg0, tainted)) hit("taint.js.xss", n);
16638
16689
  else if (!!method && MONGO_QUERY_METHODS.has(method) && exprIsTainted(arg0, tainted)) hit("taint.js.nosql_injection", n);
@@ -16712,6 +16763,7 @@ function detectTaintFromAst(ast, path, text) {
16712
16763
  fingerprint
16713
16764
  });
16714
16765
  };
16766
+ const fsNames = collectFsImportedNames(ast);
16715
16767
  const functions = collectLocalFunctions(ast);
16716
16768
  const paramSinks = /* @__PURE__ */ new Map();
16717
16769
  const hitKey2 = (h) => `${h.ruleId}@${h.node.start ?? -1}`;
@@ -16723,7 +16775,7 @@ function detectTaintFromAst(ast, path, text) {
16723
16775
  fn.params.forEach((p, i2) => {
16724
16776
  if (!p) return;
16725
16777
  const tainted = collectTainted(fn.scope, [p]);
16726
- const hits = scanSinks(fn.scope, tainted);
16778
+ const hits = scanSinks(fn.scope, tainted, fsNames);
16727
16779
  walkScope(fn.scope, (n) => {
16728
16780
  if (n.type !== "CallExpression") return;
16729
16781
  const callee = calleeName(n);
@@ -16760,7 +16812,7 @@ function detectTaintFromAst(ast, path, text) {
16760
16812
  });
16761
16813
  for (const scope of scopes) {
16762
16814
  const tainted = collectTainted(scope, []);
16763
- for (const h of scanSinks(scope, tainted)) addFinding(h.ruleId, h.node);
16815
+ for (const h of scanSinks(scope, tainted, fsNames)) addFinding(h.ruleId, h.node);
16764
16816
  walkScope(scope, (n) => {
16765
16817
  if (n.type !== "CallExpression") return;
16766
16818
  const name2 = calleeName(n);
@@ -16962,7 +17014,7 @@ function scaToCanonical(input) {
16962
17014
 
16963
17015
  // src/engine/version.ts
16964
17016
  var ENGINE_VERSION = "0.1.0";
16965
- var RULESET_VERSION = "2026.10.20-23";
17017
+ var RULESET_VERSION = "2026.10.23-24";
16966
17018
 
16967
17019
  // src/engine/suppress.ts
16968
17020
  var EMPTY_CONFIG = { excludes: [], scoped: [], problems: [] };
package/dist/server.cjs CHANGED
@@ -24351,6 +24351,24 @@ var PATTERN_RULES = [
24351
24351
  exts: JS,
24352
24352
  pattern: /createHash\s*\(\s*['"`](?:md5|sha1)['"`]\s*\)/gi
24353
24353
  },
24354
+ {
24355
+ // 2026-10-23: 自己完結コーパス(measure-gate/sast-corpus.ts)が検出欠落として
24356
+ // 検出した。pattern.go.weak_cipher と pattern.java.weak_cipher は存在するのに
24357
+ // **JS/TS 版が無く**、同じ弱点が Go/Java では検出され Node では無検出だった。
24358
+ //
24359
+ // 0-FP の作り: アルゴリズム名を**引用符の直後に固定**する。これが無いと
24360
+ // "description" のような語が `des` に当たる。安全な算法(aes-256-gcm /
24361
+ // aes-256-cbc 等)に当たらないことはテストで固定している。
24362
+ id: "pattern.js.weak_cipher",
24363
+ cwe: "CWE-327",
24364
+ owasp: "A02:2021",
24365
+ severity: "medium",
24366
+ title: "Weak cipher algorithm",
24367
+ message: "DES/3DES, RC2/RC4, Blowfish and IDEA are broken; ECB mode leaks plaintext structure.",
24368
+ remediation: "Use aes-256-gcm (AEAD) with a random IV via crypto.createCipheriv().",
24369
+ exts: JS,
24370
+ pattern: /create(?:De)?Cipher(?:iv)?\s*\(\s*['"`](?:des(?:-[a-z0-9-]*)?|rc[24](?:-[a-z0-9-]*)?|bf(?:-[a-z0-9-]*)?|blowfish|idea(?:-[a-z0-9-]*)?|[a-z0-9-]*-ecb)['"`]/gi
24371
+ },
24354
24372
  // ── Python ──
24355
24373
  {
24356
24374
  id: "pattern.py.requests_verify_false",
@@ -25691,7 +25709,39 @@ function collectTainted(scope, seed) {
25691
25709
  }
25692
25710
  return tainted;
25693
25711
  }
25694
- function scanSinks(scope, tainted) {
25712
+ function isFsModule(spec) {
25713
+ if (typeof spec !== "string") return false;
25714
+ const m = spec.replace(/^node:/, "");
25715
+ return m === "fs" || m === "fs/promises";
25716
+ }
25717
+ function collectFsImportedNames(ast) {
25718
+ const names = /* @__PURE__ */ new Set();
25719
+ walk(ast, (n) => {
25720
+ if (n.type === "ImportDeclaration" && isFsModule(n.source?.value)) {
25721
+ for (const spec of n.specifiers ?? []) {
25722
+ if (spec.type !== "ImportSpecifier") continue;
25723
+ const imported = spec.imported?.name;
25724
+ const local = spec.local?.name;
25725
+ if (typeof imported === "string" && FS_METHODS.has(imported) && typeof local === "string") names.add(local);
25726
+ }
25727
+ return;
25728
+ }
25729
+ if (n.type === "VariableDeclarator") {
25730
+ const init3 = n.init;
25731
+ const id = n.id;
25732
+ if (!init3 || !id || id.type !== "ObjectPattern") return;
25733
+ if (calleeName(init3) !== "require") return;
25734
+ if (!isFsModule(init3.arguments?.[0]?.value)) return;
25735
+ for (const prop of id.properties ?? []) {
25736
+ const key = prop.key?.name;
25737
+ const val = prop.value?.name;
25738
+ if (typeof key === "string" && FS_METHODS.has(key) && typeof val === "string") names.add(val);
25739
+ }
25740
+ }
25741
+ });
25742
+ return names;
25743
+ }
25744
+ function scanSinks(scope, tainted, fsNames = /* @__PURE__ */ new Set()) {
25695
25745
  const hits = [];
25696
25746
  const hit = (ruleId, node) => {
25697
25747
  hits.push({ ruleId, node });
@@ -25710,6 +25760,7 @@ function scanSinks(scope, tainted) {
25710
25760
  else if (memberPath(callee) === "document.write" && exprIsTainted(arg0, tainted)) hit("taint.js.xss", n);
25711
25761
  else if ((idName === "fetch" || idName === "axios" || idName === "got") && exprIsTainted(arg0, tainted) || !!method && HTTP_METHODS.has(method) && !!objPath && HTTP_CLIENT_OBJECTS.has(objPath) && exprIsTainted(arg0, tainted)) hit("taint.js.ssrf", n);
25712
25762
  else if (!!method && FS_METHODS.has(method) && !!objPath && (objPath === "fs" || objPath === "fs.promises" || objPath === "fsp") && exprIsTainted(arg0, tainted)) hit("taint.js.path_traversal", n);
25763
+ else if (!!idName && fsNames.has(idName) && exprIsTainted(arg0, tainted)) hit("taint.js.path_traversal", n);
25713
25764
  else if (REDIRECT_CALLS.has(memberPath(callee) ?? "") && exprIsTainted(arg0, tainted)) hit("taint.js.open_redirect", n);
25714
25765
  else if (!!method && ["send", "write", "end"].includes(method) && !!objPath && (objPath === "res" || objPath === "response") && exprIsTainted(arg0, tainted)) hit("taint.js.xss", n);
25715
25766
  else if (!!method && MONGO_QUERY_METHODS.has(method) && exprIsTainted(arg0, tainted)) hit("taint.js.nosql_injection", n);
@@ -25789,6 +25840,7 @@ function detectTaintFromAst(ast, path, text) {
25789
25840
  fingerprint
25790
25841
  });
25791
25842
  };
25843
+ const fsNames = collectFsImportedNames(ast);
25792
25844
  const functions = collectLocalFunctions(ast);
25793
25845
  const paramSinks = /* @__PURE__ */ new Map();
25794
25846
  const hitKey2 = (h) => `${h.ruleId}@${h.node.start ?? -1}`;
@@ -25800,7 +25852,7 @@ function detectTaintFromAst(ast, path, text) {
25800
25852
  fn.params.forEach((p, i2) => {
25801
25853
  if (!p) return;
25802
25854
  const tainted = collectTainted(fn.scope, [p]);
25803
- const hits = scanSinks(fn.scope, tainted);
25855
+ const hits = scanSinks(fn.scope, tainted, fsNames);
25804
25856
  walkScope(fn.scope, (n) => {
25805
25857
  if (n.type !== "CallExpression") return;
25806
25858
  const callee = calleeName(n);
@@ -25837,7 +25889,7 @@ function detectTaintFromAst(ast, path, text) {
25837
25889
  });
25838
25890
  for (const scope of scopes) {
25839
25891
  const tainted = collectTainted(scope, []);
25840
- for (const h of scanSinks(scope, tainted)) addFinding(h.ruleId, h.node);
25892
+ for (const h of scanSinks(scope, tainted, fsNames)) addFinding(h.ruleId, h.node);
25841
25893
  walkScope(scope, (n) => {
25842
25894
  if (n.type !== "CallExpression") return;
25843
25895
  const name2 = calleeName(n);
@@ -26039,7 +26091,7 @@ function scaToCanonical(input) {
26039
26091
 
26040
26092
  // src/engine/version.ts
26041
26093
  var ENGINE_VERSION = "0.1.0";
26042
- var RULESET_VERSION = "2026.10.20-23";
26094
+ var RULESET_VERSION = "2026.10.23-24";
26043
26095
 
26044
26096
  // src/engine/suppress.ts
26045
26097
  var SPHIORIGNORE_FILE = ".sphiorignore";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sphior/scan",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
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": {