@sphior/scan 0.1.4 → 0.1.6
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 +283 -19
- package/dist/server.cjs +170 -8
- package/package.json +7 -4
package/dist/cli.js
CHANGED
|
@@ -14650,6 +14650,30 @@ function shannonEntropy(s) {
|
|
|
14650
14650
|
}
|
|
14651
14651
|
return h;
|
|
14652
14652
|
}
|
|
14653
|
+
var PLACEHOLDER_MARKERS = [
|
|
14654
|
+
"your",
|
|
14655
|
+
"here",
|
|
14656
|
+
"example",
|
|
14657
|
+
"sample",
|
|
14658
|
+
"placeholder",
|
|
14659
|
+
"dummy",
|
|
14660
|
+
"changeme",
|
|
14661
|
+
"change_me",
|
|
14662
|
+
"hardcoded",
|
|
14663
|
+
"redacted",
|
|
14664
|
+
"insert",
|
|
14665
|
+
"replace",
|
|
14666
|
+
"todo",
|
|
14667
|
+
"fixme",
|
|
14668
|
+
"xxxx",
|
|
14669
|
+
"...",
|
|
14670
|
+
"<",
|
|
14671
|
+
">"
|
|
14672
|
+
];
|
|
14673
|
+
function looksLikePlaceholder(s) {
|
|
14674
|
+
const lower = s.toLowerCase();
|
|
14675
|
+
return PLACEHOLDER_MARKERS.some((m) => lower.includes(m));
|
|
14676
|
+
}
|
|
14653
14677
|
function looksHighEntropy(s, minLen = 16, minEntropy = 3.5) {
|
|
14654
14678
|
if (s.length < minLen) return false;
|
|
14655
14679
|
if (!/^[A-Za-z0-9+/=_\-.]+$/.test(s)) return false;
|
|
@@ -14796,7 +14820,20 @@ var SECRET_RULES = [
|
|
|
14796
14820
|
provider: "PEM",
|
|
14797
14821
|
title: "Private Key",
|
|
14798
14822
|
severity: "critical",
|
|
14799
|
-
|
|
14823
|
+
// PEM ヘッダ**単体**では鍵ではない。実測(自社リポジトリ 1 回の走査)で
|
|
14824
|
+
// secret.private_key は 6 件すべて誤検知だった:
|
|
14825
|
+
// - 日本語コメント中の「-----BEGIN RSA PRIVATE KEY----- で配布される」という説明
|
|
14826
|
+
// - `.replace(/-----BEGIN PRIVATE KEY-----/g, '')` という除去コード
|
|
14827
|
+
// - `const PEM = "-----BEGIN RSA PRIVATE KEY-----"` 等のテストベクタ(本体なし)
|
|
14828
|
+
// - ヘッダ + `btoa(...)` のようにコードで本体を作る箇所
|
|
14829
|
+
// severity=critical のルールが 6/6 外れると、本物(この走査で実際に見つかった
|
|
14830
|
+
// 本番 Stripe キー等)が埋まる。そこで「ヘッダの直後に base64 の鍵本体が続く」
|
|
14831
|
+
// ことを先読みで要求する。
|
|
14832
|
+
//
|
|
14833
|
+
// 先読み(キャプチャしない)にしてある点が重要: group 1 はヘッダのままなので
|
|
14834
|
+
// snippet_hash と fingerprint が変わらず、既存の真検出の delta 追跡が途切れない。
|
|
14835
|
+
// 区切りは実鍵の書かれ方に合わせる — 生改行・`\n` のエスケープ列・引用符・カンマ。
|
|
14836
|
+
pattern: /(-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----)(?=(?:\s|\\+[nrt]|["'`,;+]){0,40}[A-Za-z0-9+/]{40,})/g,
|
|
14800
14837
|
group: 1,
|
|
14801
14838
|
references: REF_CWE798
|
|
14802
14839
|
},
|
|
@@ -15053,7 +15090,7 @@ function detectSecrets(path, text, opts = {}) {
|
|
|
15053
15090
|
const g = rule.group ?? 0;
|
|
15054
15091
|
const secret = m[g];
|
|
15055
15092
|
if (!secret) continue;
|
|
15056
|
-
if (PLACEHOLDER_SECRETS.has(secret)) continue;
|
|
15093
|
+
if (PLACEHOLDER_SECRETS.has(secret) || looksLikePlaceholder(secret)) continue;
|
|
15057
15094
|
const absStart = m.index + m[0].indexOf(secret);
|
|
15058
15095
|
push(build(path, rule, secret, absStart, starts, false));
|
|
15059
15096
|
if (re.lastIndex === m.index) re.lastIndex++;
|
|
@@ -15064,7 +15101,7 @@ function detectSecrets(path, text, opts = {}) {
|
|
|
15064
15101
|
let m;
|
|
15065
15102
|
while ((m = re.exec(text)) !== null) {
|
|
15066
15103
|
const secret = m[1];
|
|
15067
|
-
if (!secret || PLACEHOLDER_SECRETS.has(secret)) continue;
|
|
15104
|
+
if (!secret || PLACEHOLDER_SECRETS.has(secret) || looksLikePlaceholder(secret)) continue;
|
|
15068
15105
|
if (!looksHighEntropy(secret)) continue;
|
|
15069
15106
|
const absStart = m.index + m[0].lastIndexOf(secret);
|
|
15070
15107
|
push(build(path, GENERIC_ASSIGNMENT, secret, absStart, starts, true));
|
|
@@ -16460,6 +16497,7 @@ var SOURCE_CALL_SUFFIXES = [
|
|
|
16460
16497
|
"formData.get",
|
|
16461
16498
|
"urlParams.get"
|
|
16462
16499
|
];
|
|
16500
|
+
var TAINT_WRAPPER_CTORS = /* @__PURE__ */ new Set(["URLSearchParams", "URL"]);
|
|
16463
16501
|
var SCOPE_TYPES = /* @__PURE__ */ new Set([
|
|
16464
16502
|
"FunctionDeclaration",
|
|
16465
16503
|
"FunctionExpression",
|
|
@@ -16517,6 +16555,11 @@ function exprIsTainted(node, tainted) {
|
|
|
16517
16555
|
return exprIsTainted(node.left, tainted) || exprIsTainted(node.right, tainted);
|
|
16518
16556
|
case "ConditionalExpression":
|
|
16519
16557
|
return exprIsTainted(node.consequent, tainted) || exprIsTainted(node.alternate, tainted);
|
|
16558
|
+
case "NewExpression": {
|
|
16559
|
+
const ctor = calleeName(node);
|
|
16560
|
+
if (!ctor || !TAINT_WRAPPER_CTORS.has(ctor)) return false;
|
|
16561
|
+
return (node.arguments ?? []).some((a) => exprIsTainted(a, tainted));
|
|
16562
|
+
}
|
|
16520
16563
|
case "ParenthesizedExpression":
|
|
16521
16564
|
case "TSAsExpression":
|
|
16522
16565
|
case "TSNonNullExpression":
|
|
@@ -16775,6 +16818,11 @@ function inLineComment(text, lineStart, matchIndex, isPython) {
|
|
|
16775
16818
|
const quotes = (upto.match(/["'`]/g) ?? []).length;
|
|
16776
16819
|
return quotes % 2 === 0;
|
|
16777
16820
|
}
|
|
16821
|
+
function isBlockCommentLine(text, lineStart) {
|
|
16822
|
+
const lineEnd = text.indexOf("\n", lineStart);
|
|
16823
|
+
const line = text.slice(lineStart, lineEnd < 0 ? text.length : lineEnd).trimStart();
|
|
16824
|
+
return line.startsWith("/*") || /^\*(\s|\/|$)/.test(line);
|
|
16825
|
+
}
|
|
16778
16826
|
function detectPatterns(path, text, opts = {}) {
|
|
16779
16827
|
if (opts.ignorePaths?.some((re) => re.test(path))) return [];
|
|
16780
16828
|
const ext = extOf(path);
|
|
@@ -16818,7 +16866,7 @@ function detectPatterns(path, text, opts = {}) {
|
|
|
16818
16866
|
while ((m = re.exec(text)) !== null) {
|
|
16819
16867
|
const idx = m.index;
|
|
16820
16868
|
const pos = toPos2(starts, idx);
|
|
16821
|
-
if (inLineComment(text, pos.lineStart, idx, isPython)) {
|
|
16869
|
+
if (inLineComment(text, pos.lineStart, idx, isPython) || isBlockCommentLine(text, pos.lineStart)) {
|
|
16822
16870
|
if (re.lastIndex === idx) re.lastIndex++;
|
|
16823
16871
|
continue;
|
|
16824
16872
|
}
|
|
@@ -16914,7 +16962,160 @@ function scaToCanonical(input) {
|
|
|
16914
16962
|
|
|
16915
16963
|
// src/engine/version.ts
|
|
16916
16964
|
var ENGINE_VERSION = "0.1.0";
|
|
16917
|
-
var RULESET_VERSION = "2026.
|
|
16965
|
+
var RULESET_VERSION = "2026.10.20-23";
|
|
16966
|
+
|
|
16967
|
+
// src/engine/suppress.ts
|
|
16968
|
+
var EMPTY_CONFIG = { excludes: [], scoped: [], problems: [] };
|
|
16969
|
+
var SPHIORIGNORE_FILE = ".sphiorignore";
|
|
16970
|
+
function escapeRe(s) {
|
|
16971
|
+
return s.replace(/[.+^$(){}|[\]\\]/g, "\\$&");
|
|
16972
|
+
}
|
|
16973
|
+
function globToRegExp(glob) {
|
|
16974
|
+
let out2 = "";
|
|
16975
|
+
for (let i2 = 0; i2 < glob.length; i2++) {
|
|
16976
|
+
const c = glob[i2];
|
|
16977
|
+
if (c === "*") {
|
|
16978
|
+
if (glob[i2 + 1] === "*") {
|
|
16979
|
+
if (glob[i2 + 2] === "/") {
|
|
16980
|
+
out2 += "(?:[^/]+/)*";
|
|
16981
|
+
i2 += 2;
|
|
16982
|
+
} else {
|
|
16983
|
+
out2 += ".*";
|
|
16984
|
+
i2 += 1;
|
|
16985
|
+
}
|
|
16986
|
+
} else {
|
|
16987
|
+
out2 += "[^/]*";
|
|
16988
|
+
}
|
|
16989
|
+
continue;
|
|
16990
|
+
}
|
|
16991
|
+
if (c === "?") {
|
|
16992
|
+
out2 += "[^/]";
|
|
16993
|
+
continue;
|
|
16994
|
+
}
|
|
16995
|
+
out2 += escapeRe(c);
|
|
16996
|
+
}
|
|
16997
|
+
return new RegExp(`^${out2}$`);
|
|
16998
|
+
}
|
|
16999
|
+
function globMatches(glob, relPath) {
|
|
17000
|
+
const g = glob.endsWith("/") ? `${glob.slice(0, -1)}` : glob;
|
|
17001
|
+
if (globToRegExp(g).test(relPath)) return true;
|
|
17002
|
+
return globToRegExp(`${g}/**`).test(relPath);
|
|
17003
|
+
}
|
|
17004
|
+
function ruleMatches(pattern, ruleId) {
|
|
17005
|
+
if (pattern.endsWith("*")) return ruleId.startsWith(pattern.slice(0, -1));
|
|
17006
|
+
return pattern === ruleId;
|
|
17007
|
+
}
|
|
17008
|
+
function looksLikeRuleId(tok) {
|
|
17009
|
+
return /^[A-Za-z][A-Za-z0-9_.-]*\*?$/.test(tok) && !tok.includes("/");
|
|
17010
|
+
}
|
|
17011
|
+
function parseSphiorignore(text) {
|
|
17012
|
+
const excludes = [];
|
|
17013
|
+
const scoped = [];
|
|
17014
|
+
const problems = [];
|
|
17015
|
+
const lines = text.split(/\r?\n/);
|
|
17016
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
17017
|
+
const raw = lines[i2];
|
|
17018
|
+
const lineNo = i2 + 1;
|
|
17019
|
+
if (!raw.trim() || raw.trim().startsWith("#")) continue;
|
|
17020
|
+
const hashAt = raw.indexOf("#");
|
|
17021
|
+
const body2 = (hashAt >= 0 ? raw.slice(0, hashAt) : raw).trim();
|
|
17022
|
+
const justification = hashAt >= 0 ? raw.slice(hashAt + 1).trim() || void 0 : void 0;
|
|
17023
|
+
if (!body2) continue;
|
|
17024
|
+
const tokens = body2.split(/\s+/);
|
|
17025
|
+
if (tokens.length > 2) {
|
|
17026
|
+
problems.push({ line: lineNo, text: body2, reason: "expected `<glob> [rule_id]` (too many fields)" });
|
|
17027
|
+
continue;
|
|
17028
|
+
}
|
|
17029
|
+
const [glob, ruleId] = tokens;
|
|
17030
|
+
if (glob.startsWith("/")) {
|
|
17031
|
+
problems.push({ line: lineNo, text: body2, reason: "paths are relative to the scan root \u2014 drop the leading `/`" });
|
|
17032
|
+
continue;
|
|
17033
|
+
}
|
|
17034
|
+
if (ruleId !== void 0 && !looksLikeRuleId(ruleId)) {
|
|
17035
|
+
problems.push({ line: lineNo, text: body2, reason: `not a rule id: ${ruleId}` });
|
|
17036
|
+
continue;
|
|
17037
|
+
}
|
|
17038
|
+
const entry = { line: lineNo, glob, ruleId, justification };
|
|
17039
|
+
(ruleId === void 0 ? excludes : scoped).push(entry);
|
|
17040
|
+
}
|
|
17041
|
+
return { excludes, scoped, problems };
|
|
17042
|
+
}
|
|
17043
|
+
function makePathExcluder(cfg) {
|
|
17044
|
+
if (cfg.excludes.length === 0) return () => false;
|
|
17045
|
+
return (relPath) => cfg.excludes.some((e) => globMatches(e.glob, relPath));
|
|
17046
|
+
}
|
|
17047
|
+
function configSuppressionFor(cfg, path, ruleId) {
|
|
17048
|
+
return cfg.scoped.find((e) => e.ruleId && ruleMatches(e.ruleId, ruleId) && globMatches(e.glob, path));
|
|
17049
|
+
}
|
|
17050
|
+
var INLINE_RE = /sphior-ignore(-next-line)?(?:[ \t]+([A-Za-z][A-Za-z0-9_.-]*\*?))?(?:[ \t]*:[ \t]*(.*))?$/;
|
|
17051
|
+
function parseInlineSuppression(lineText) {
|
|
17052
|
+
const m = INLINE_RE.exec(lineText);
|
|
17053
|
+
if (!m) return void 0;
|
|
17054
|
+
return {
|
|
17055
|
+
nextLine: m[1] === "-next-line",
|
|
17056
|
+
ruleId: m[2],
|
|
17057
|
+
justification: m[3]?.trim() || void 0
|
|
17058
|
+
};
|
|
17059
|
+
}
|
|
17060
|
+
function inlineSuppressionFor(lines, startLine, ruleId) {
|
|
17061
|
+
const own = lines[startLine - 1] !== void 0 ? parseInlineSuppression(lines[startLine - 1]) : void 0;
|
|
17062
|
+
if (own && !own.nextLine && (!own.ruleId || ruleMatches(own.ruleId, ruleId))) return own;
|
|
17063
|
+
const prev = startLine >= 2 && lines[startLine - 2] !== void 0 ? parseInlineSuppression(lines[startLine - 2]) : void 0;
|
|
17064
|
+
if (prev && prev.nextLine && (!prev.ruleId || ruleMatches(prev.ruleId, ruleId))) return prev;
|
|
17065
|
+
return void 0;
|
|
17066
|
+
}
|
|
17067
|
+
function applySuppressions(findings, input = {}) {
|
|
17068
|
+
const cfg = input.config;
|
|
17069
|
+
const hasConfig = !!cfg && cfg.scoped.length > 0;
|
|
17070
|
+
const hasText = !!input.textByPath && input.textByPath.size > 0;
|
|
17071
|
+
if (!hasConfig && !hasText) return { active: findings, suppressed: [] };
|
|
17072
|
+
const linesCache = /* @__PURE__ */ new Map();
|
|
17073
|
+
const linesOf = (path) => {
|
|
17074
|
+
const cached = linesCache.get(path);
|
|
17075
|
+
if (cached) return cached;
|
|
17076
|
+
const text = input.textByPath?.get(path);
|
|
17077
|
+
if (text === void 0) return void 0;
|
|
17078
|
+
const lines = text.split(/\r?\n/);
|
|
17079
|
+
linesCache.set(path, lines);
|
|
17080
|
+
return lines;
|
|
17081
|
+
};
|
|
17082
|
+
const active = [];
|
|
17083
|
+
const suppressed = [];
|
|
17084
|
+
for (const f of findings) {
|
|
17085
|
+
const byConfig = hasConfig ? configSuppressionFor(cfg, f.location.path, f.rule_id) : void 0;
|
|
17086
|
+
if (byConfig) {
|
|
17087
|
+
suppressed.push({
|
|
17088
|
+
...f,
|
|
17089
|
+
suppression: {
|
|
17090
|
+
by: "config",
|
|
17091
|
+
rule_scoped: true,
|
|
17092
|
+
justification: byConfig.justification,
|
|
17093
|
+
source: `${SPHIORIGNORE_FILE}:${byConfig.line}`
|
|
17094
|
+
}
|
|
17095
|
+
});
|
|
17096
|
+
continue;
|
|
17097
|
+
}
|
|
17098
|
+
const lines = linesOf(f.location.path);
|
|
17099
|
+
const byInline = lines ? inlineSuppressionFor(lines, f.location.start_line, f.rule_id) : void 0;
|
|
17100
|
+
if (byInline) {
|
|
17101
|
+
suppressed.push({
|
|
17102
|
+
...f,
|
|
17103
|
+
suppression: {
|
|
17104
|
+
by: "inline",
|
|
17105
|
+
rule_scoped: !!byInline.ruleId,
|
|
17106
|
+
justification: byInline.justification,
|
|
17107
|
+
source: `${f.location.path}:${byInline.nextLine ? f.location.start_line - 1 : f.location.start_line}`
|
|
17108
|
+
}
|
|
17109
|
+
});
|
|
17110
|
+
continue;
|
|
17111
|
+
}
|
|
17112
|
+
active.push(f);
|
|
17113
|
+
}
|
|
17114
|
+
return { active, suppressed };
|
|
17115
|
+
}
|
|
17116
|
+
function countWithoutJustification(suppressed) {
|
|
17117
|
+
return suppressed.reduce((n, f) => f.suppression && !f.suppression.justification ? n + 1 : n, 0);
|
|
17118
|
+
}
|
|
16918
17119
|
|
|
16919
17120
|
// src/engine/run.ts
|
|
16920
17121
|
var kindOrder = {
|
|
@@ -16926,43 +17127,79 @@ var kindOrder = {
|
|
|
16926
17127
|
function runScan(input) {
|
|
16927
17128
|
const files = input.files ?? [];
|
|
16928
17129
|
const opts = { allowFingerprints: input.allowFingerprints, ignorePaths: input.ignorePaths };
|
|
16929
|
-
const
|
|
17130
|
+
const detected = [
|
|
16930
17131
|
...detectSecretsInFiles(files, opts),
|
|
16931
17132
|
...detectPatternsInFiles(files, opts),
|
|
16932
17133
|
...scaToCanonical(input.sca ?? [])
|
|
16933
17134
|
];
|
|
16934
|
-
|
|
17135
|
+
detected.sort((a, b) => {
|
|
16935
17136
|
if (a.location.path !== b.location.path) return a.location.path < b.location.path ? -1 : 1;
|
|
16936
17137
|
if (a.location.start_line !== b.location.start_line) return a.location.start_line - b.location.start_line;
|
|
16937
17138
|
if (a.location.start_col !== b.location.start_col) return a.location.start_col - b.location.start_col;
|
|
16938
17139
|
if (kindOrder[a.kind] !== kindOrder[b.kind]) return kindOrder[a.kind] - kindOrder[b.kind];
|
|
16939
17140
|
return a.rule_id < b.rule_id ? -1 : a.rule_id > b.rule_id ? 1 : 0;
|
|
16940
17141
|
});
|
|
17142
|
+
const { active: findings, suppressed } = applySuppressions(detected, {
|
|
17143
|
+
config: input.suppress,
|
|
17144
|
+
textByPath: new Map(files.map((f) => [f.path, f.text]))
|
|
17145
|
+
});
|
|
16941
17146
|
const sarif = toSarif(findings, { engineVersion: ENGINE_VERSION, rulesetVersion: RULESET_VERSION });
|
|
16942
|
-
return { findings, sarif, engine_version: ENGINE_VERSION, ruleset_version: RULESET_VERSION };
|
|
17147
|
+
return { findings, suppressed, sarif, engine_version: ENGINE_VERSION, ruleset_version: RULESET_VERSION };
|
|
16943
17148
|
}
|
|
16944
17149
|
|
|
16945
17150
|
// src/engine/collect.ts
|
|
16946
17151
|
import { readdirSync, statSync, readFileSync } from "node:fs";
|
|
16947
17152
|
import { join, relative, sep } from "node:path";
|
|
16948
17153
|
var IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
17154
|
+
// VCS
|
|
16949
17155
|
".git",
|
|
17156
|
+
".hg",
|
|
17157
|
+
".svn",
|
|
17158
|
+
// JS/TS の依存と出力
|
|
16950
17159
|
"node_modules",
|
|
17160
|
+
"bower_components",
|
|
17161
|
+
".yarn",
|
|
17162
|
+
".pnp",
|
|
17163
|
+
".pnpm-store",
|
|
16951
17164
|
"dist",
|
|
16952
17165
|
"build",
|
|
16953
|
-
".next",
|
|
16954
17166
|
"out",
|
|
16955
17167
|
"coverage",
|
|
16956
|
-
"
|
|
17168
|
+
".cache",
|
|
17169
|
+
".turbo",
|
|
17170
|
+
".swc",
|
|
17171
|
+
".parcel-cache",
|
|
17172
|
+
// フレームワーク/デプロイのビルド出力
|
|
17173
|
+
".next",
|
|
17174
|
+
".open-next",
|
|
17175
|
+
".vercel",
|
|
17176
|
+
".wrangler",
|
|
17177
|
+
".serverless",
|
|
17178
|
+
".sst",
|
|
17179
|
+
".svelte-kit",
|
|
17180
|
+
".nuxt",
|
|
17181
|
+
".output",
|
|
17182
|
+
".astro",
|
|
17183
|
+
".docusaurus",
|
|
17184
|
+
// Python
|
|
16957
17185
|
".venv",
|
|
16958
17186
|
"venv",
|
|
16959
17187
|
"__pycache__",
|
|
17188
|
+
".mypy_cache",
|
|
17189
|
+
".pytest_cache",
|
|
17190
|
+
".ruff_cache",
|
|
17191
|
+
".tox",
|
|
17192
|
+
".nox",
|
|
17193
|
+
".eggs",
|
|
17194
|
+
// JVM / その他言語
|
|
16960
17195
|
"target",
|
|
16961
|
-
".
|
|
16962
|
-
"
|
|
16963
|
-
"
|
|
16964
|
-
".
|
|
16965
|
-
"
|
|
17196
|
+
".gradle",
|
|
17197
|
+
"vendor",
|
|
17198
|
+
"Pods",
|
|
17199
|
+
".dart_tool",
|
|
17200
|
+
".pub-cache",
|
|
17201
|
+
// IaC のプロバイダバイナリ(`.tf` 本体は配下に無いので検査対象は失わない)
|
|
17202
|
+
".terraform"
|
|
16966
17203
|
]);
|
|
16967
17204
|
function toPosix(p) {
|
|
16968
17205
|
return sep === "/" ? p : p.split(sep).join("/");
|
|
@@ -16993,9 +17230,11 @@ function collectSourceFiles(rootDir, opts = {}) {
|
|
|
16993
17230
|
}
|
|
16994
17231
|
if (st.isDirectory()) {
|
|
16995
17232
|
if (ignore.has(name2)) continue;
|
|
17233
|
+
if (opts.exclude?.(toPosix(relative(rootDir, full)))) continue;
|
|
16996
17234
|
walk2(full);
|
|
16997
17235
|
} else if (st.isFile()) {
|
|
16998
17236
|
if (st.size > maxBytes) continue;
|
|
17237
|
+
if (opts.exclude?.(toPosix(relative(rootDir, full)))) continue;
|
|
16999
17238
|
let buf;
|
|
17000
17239
|
try {
|
|
17001
17240
|
buf = readFileSync(full);
|
|
@@ -17011,6 +17250,13 @@ function collectSourceFiles(rootDir, opts = {}) {
|
|
|
17011
17250
|
files.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
17012
17251
|
return files;
|
|
17013
17252
|
}
|
|
17253
|
+
function readSphiorignore(rootDir) {
|
|
17254
|
+
try {
|
|
17255
|
+
return parseSphiorignore(readFileSync(join(rootDir, SPHIORIGNORE_FILE), "utf8"));
|
|
17256
|
+
} catch {
|
|
17257
|
+
return EMPTY_CONFIG;
|
|
17258
|
+
}
|
|
17259
|
+
}
|
|
17014
17260
|
|
|
17015
17261
|
// src/engine/cli.ts
|
|
17016
17262
|
import { existsSync, statSync as statSync2, writeFileSync } from "node:fs";
|
|
@@ -17165,7 +17411,7 @@ function runCli(argv, ctx = {}) {
|
|
|
17165
17411
|
const writeFile = ctx.writeFile ?? ((p, d) => writeFileSync(p, d));
|
|
17166
17412
|
const [cmd, dir, ...rest] = argv;
|
|
17167
17413
|
if (cmd !== "run" || !dir) {
|
|
17168
|
-
logErr("usage: sphior-scan run <dir> [--sarif out.sarif] [--json out.json] [--fix-context out.json] [--fail-on high]");
|
|
17414
|
+
logErr("usage: sphior-scan run <dir> [--sarif out.sarif] [--json out.json] [--fix-context out.json] [--suppressed out.json] [--fail-on high] [--no-ignore-file]");
|
|
17169
17415
|
return { exitCode: 2, error: "bad_args" };
|
|
17170
17416
|
}
|
|
17171
17417
|
const flags2 = parseFlags(rest);
|
|
@@ -17174,11 +17420,23 @@ function runCli(argv, ctx = {}) {
|
|
|
17174
17420
|
logErr(" Nothing was scanned, so this is not a clean result. Check the path.");
|
|
17175
17421
|
return { exitCode: 2, error: "dir_not_found" };
|
|
17176
17422
|
}
|
|
17177
|
-
const
|
|
17178
|
-
const
|
|
17423
|
+
const ignoreCfg = flags2["no-ignore-file"] ? EMPTY_CONFIG : readSphiorignore(dir);
|
|
17424
|
+
const isExcluded = makePathExcluder(ignoreCfg);
|
|
17425
|
+
let excludedCount = 0;
|
|
17426
|
+
const exclude = (relPath) => {
|
|
17427
|
+
if (!isExcluded(relPath)) return false;
|
|
17428
|
+
excludedCount++;
|
|
17429
|
+
return true;
|
|
17430
|
+
};
|
|
17431
|
+
for (const p of ignoreCfg.problems) {
|
|
17432
|
+
logErr(`warning: ${SPHIORIGNORE_FILE}:${p.line}: ${p.reason} \u2014 ignored this line (\u201C${p.text}\u201D)`);
|
|
17433
|
+
}
|
|
17434
|
+
const files = collectSourceFiles(dir, { exclude });
|
|
17435
|
+
const result = runScan({ files, suppress: ignoreCfg });
|
|
17179
17436
|
const sarifJson = canonicalJson(result.sarif);
|
|
17180
17437
|
if (flags2.sarif) writeFile(flags2.sarif, sarifJson);
|
|
17181
17438
|
if (flags2.json) writeFile(flags2.json, canonicalJson(result.findings));
|
|
17439
|
+
if (flags2.suppressed) writeFile(flags2.suppressed, canonicalJson(result.suppressed));
|
|
17182
17440
|
if (flags2["fix-context"]) writeFile(flags2["fix-context"], canonicalJson(buildFixContexts(result.findings)));
|
|
17183
17441
|
if (!flags2.sarif && !flags2.json && !flags2["fix-context"]) {
|
|
17184
17442
|
if (ctx.isTty) {
|
|
@@ -17201,14 +17459,20 @@ function runCli(argv, ctx = {}) {
|
|
|
17201
17459
|
acc[f.severity] = (acc[f.severity] ?? 0) + 1;
|
|
17202
17460
|
return acc;
|
|
17203
17461
|
}, {});
|
|
17462
|
+
const scope = (excludedCount > 0 ? ` ${excludedCount} files excluded` : "") + (result.suppressed.length > 0 ? `${excludedCount > 0 ? "," : ""} ${result.suppressed.length} suppressed` : "");
|
|
17204
17463
|
logErr(
|
|
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}`
|
|
17464
|
+
`SPHIOR CODE: ${result.findings.length} findings (critical=${counts.critical ?? 0} high=${counts.high ?? 0} medium=${counts.medium ?? 0} low=${counts.low ?? 0}) ${files.length} files scanned${scope ? ` \u2022${scope}` : ""} engine=${result.engine_version} ruleset=${result.ruleset_version}`
|
|
17206
17465
|
);
|
|
17466
|
+
const unexplained = countWithoutJustification(result.suppressed);
|
|
17467
|
+
if (unexplained > 0) {
|
|
17468
|
+
logErr(` note: ${unexplained} of the suppressions carry no reason. Add \`: why\` so an auditor can read it later.`);
|
|
17469
|
+
}
|
|
17207
17470
|
if (ctx.isTty && !flags2.sarif && !flags2.json && !flags2["fix-context"]) {
|
|
17208
17471
|
logErr("");
|
|
17209
17472
|
logErr(" --fix-context fc.json hand findings to an AI agent to fix");
|
|
17210
17473
|
logErr(" --sarif out.sarif for GitHub code scanning / CI dashboards");
|
|
17211
17474
|
logErr(" --fail-on high exit 1 if anything at or above this severity (CI)");
|
|
17475
|
+
logErr(` ${SPHIORIGNORE_FILE} exclude paths, or accept a rule with a reason`);
|
|
17212
17476
|
}
|
|
17213
17477
|
let exitCode = 0;
|
|
17214
17478
|
const failOn = flags2["fail-on"];
|
package/dist/server.cjs
CHANGED
|
@@ -23727,6 +23727,30 @@ function shannonEntropy(s) {
|
|
|
23727
23727
|
}
|
|
23728
23728
|
return h;
|
|
23729
23729
|
}
|
|
23730
|
+
var PLACEHOLDER_MARKERS = [
|
|
23731
|
+
"your",
|
|
23732
|
+
"here",
|
|
23733
|
+
"example",
|
|
23734
|
+
"sample",
|
|
23735
|
+
"placeholder",
|
|
23736
|
+
"dummy",
|
|
23737
|
+
"changeme",
|
|
23738
|
+
"change_me",
|
|
23739
|
+
"hardcoded",
|
|
23740
|
+
"redacted",
|
|
23741
|
+
"insert",
|
|
23742
|
+
"replace",
|
|
23743
|
+
"todo",
|
|
23744
|
+
"fixme",
|
|
23745
|
+
"xxxx",
|
|
23746
|
+
"...",
|
|
23747
|
+
"<",
|
|
23748
|
+
">"
|
|
23749
|
+
];
|
|
23750
|
+
function looksLikePlaceholder(s) {
|
|
23751
|
+
const lower = s.toLowerCase();
|
|
23752
|
+
return PLACEHOLDER_MARKERS.some((m) => lower.includes(m));
|
|
23753
|
+
}
|
|
23730
23754
|
function looksHighEntropy(s, minLen = 16, minEntropy = 3.5) {
|
|
23731
23755
|
if (s.length < minLen) return false;
|
|
23732
23756
|
if (!/^[A-Za-z0-9+/=_\-.]+$/.test(s)) return false;
|
|
@@ -23873,7 +23897,20 @@ var SECRET_RULES = [
|
|
|
23873
23897
|
provider: "PEM",
|
|
23874
23898
|
title: "Private Key",
|
|
23875
23899
|
severity: "critical",
|
|
23876
|
-
|
|
23900
|
+
// PEM ヘッダ**単体**では鍵ではない。実測(自社リポジトリ 1 回の走査)で
|
|
23901
|
+
// secret.private_key は 6 件すべて誤検知だった:
|
|
23902
|
+
// - 日本語コメント中の「-----BEGIN RSA PRIVATE KEY----- で配布される」という説明
|
|
23903
|
+
// - `.replace(/-----BEGIN PRIVATE KEY-----/g, '')` という除去コード
|
|
23904
|
+
// - `const PEM = "-----BEGIN RSA PRIVATE KEY-----"` 等のテストベクタ(本体なし)
|
|
23905
|
+
// - ヘッダ + `btoa(...)` のようにコードで本体を作る箇所
|
|
23906
|
+
// severity=critical のルールが 6/6 外れると、本物(この走査で実際に見つかった
|
|
23907
|
+
// 本番 Stripe キー等)が埋まる。そこで「ヘッダの直後に base64 の鍵本体が続く」
|
|
23908
|
+
// ことを先読みで要求する。
|
|
23909
|
+
//
|
|
23910
|
+
// 先読み(キャプチャしない)にしてある点が重要: group 1 はヘッダのままなので
|
|
23911
|
+
// snippet_hash と fingerprint が変わらず、既存の真検出の delta 追跡が途切れない。
|
|
23912
|
+
// 区切りは実鍵の書かれ方に合わせる — 生改行・`\n` のエスケープ列・引用符・カンマ。
|
|
23913
|
+
pattern: /(-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----)(?=(?:\s|\\+[nrt]|["'`,;+]){0,40}[A-Za-z0-9+/]{40,})/g,
|
|
23877
23914
|
group: 1,
|
|
23878
23915
|
references: REF_CWE798
|
|
23879
23916
|
},
|
|
@@ -24130,7 +24167,7 @@ function detectSecrets(path, text, opts = {}) {
|
|
|
24130
24167
|
const g = rule.group ?? 0;
|
|
24131
24168
|
const secret = m[g];
|
|
24132
24169
|
if (!secret) continue;
|
|
24133
|
-
if (PLACEHOLDER_SECRETS.has(secret)) continue;
|
|
24170
|
+
if (PLACEHOLDER_SECRETS.has(secret) || looksLikePlaceholder(secret)) continue;
|
|
24134
24171
|
const absStart = m.index + m[0].indexOf(secret);
|
|
24135
24172
|
push(build(path, rule, secret, absStart, starts, false));
|
|
24136
24173
|
if (re.lastIndex === m.index) re.lastIndex++;
|
|
@@ -24141,7 +24178,7 @@ function detectSecrets(path, text, opts = {}) {
|
|
|
24141
24178
|
let m;
|
|
24142
24179
|
while ((m = re.exec(text)) !== null) {
|
|
24143
24180
|
const secret = m[1];
|
|
24144
|
-
if (!secret || PLACEHOLDER_SECRETS.has(secret)) continue;
|
|
24181
|
+
if (!secret || PLACEHOLDER_SECRETS.has(secret) || looksLikePlaceholder(secret)) continue;
|
|
24145
24182
|
if (!looksHighEntropy(secret)) continue;
|
|
24146
24183
|
const absStart = m.index + m[0].lastIndexOf(secret);
|
|
24147
24184
|
push(build(path, GENERIC_ASSIGNMENT, secret, absStart, starts, true));
|
|
@@ -25537,6 +25574,7 @@ var SOURCE_CALL_SUFFIXES = [
|
|
|
25537
25574
|
"formData.get",
|
|
25538
25575
|
"urlParams.get"
|
|
25539
25576
|
];
|
|
25577
|
+
var TAINT_WRAPPER_CTORS = /* @__PURE__ */ new Set(["URLSearchParams", "URL"]);
|
|
25540
25578
|
var SCOPE_TYPES = /* @__PURE__ */ new Set([
|
|
25541
25579
|
"FunctionDeclaration",
|
|
25542
25580
|
"FunctionExpression",
|
|
@@ -25594,6 +25632,11 @@ function exprIsTainted(node, tainted) {
|
|
|
25594
25632
|
return exprIsTainted(node.left, tainted) || exprIsTainted(node.right, tainted);
|
|
25595
25633
|
case "ConditionalExpression":
|
|
25596
25634
|
return exprIsTainted(node.consequent, tainted) || exprIsTainted(node.alternate, tainted);
|
|
25635
|
+
case "NewExpression": {
|
|
25636
|
+
const ctor = calleeName(node);
|
|
25637
|
+
if (!ctor || !TAINT_WRAPPER_CTORS.has(ctor)) return false;
|
|
25638
|
+
return (node.arguments ?? []).some((a) => exprIsTainted(a, tainted));
|
|
25639
|
+
}
|
|
25597
25640
|
case "ParenthesizedExpression":
|
|
25598
25641
|
case "TSAsExpression":
|
|
25599
25642
|
case "TSNonNullExpression":
|
|
@@ -25852,6 +25895,11 @@ function inLineComment(text, lineStart, matchIndex, isPython) {
|
|
|
25852
25895
|
const quotes = (upto.match(/["'`]/g) ?? []).length;
|
|
25853
25896
|
return quotes % 2 === 0;
|
|
25854
25897
|
}
|
|
25898
|
+
function isBlockCommentLine(text, lineStart) {
|
|
25899
|
+
const lineEnd = text.indexOf("\n", lineStart);
|
|
25900
|
+
const line = text.slice(lineStart, lineEnd < 0 ? text.length : lineEnd).trimStart();
|
|
25901
|
+
return line.startsWith("/*") || /^\*(\s|\/|$)/.test(line);
|
|
25902
|
+
}
|
|
25855
25903
|
function detectPatterns(path, text, opts = {}) {
|
|
25856
25904
|
if (opts.ignorePaths?.some((re) => re.test(path))) return [];
|
|
25857
25905
|
const ext = extOf(path);
|
|
@@ -25895,7 +25943,7 @@ function detectPatterns(path, text, opts = {}) {
|
|
|
25895
25943
|
while ((m = re.exec(text)) !== null) {
|
|
25896
25944
|
const idx = m.index;
|
|
25897
25945
|
const pos = toPos2(starts, idx);
|
|
25898
|
-
if (inLineComment(text, pos.lineStart, idx, isPython)) {
|
|
25946
|
+
if (inLineComment(text, pos.lineStart, idx, isPython) || isBlockCommentLine(text, pos.lineStart)) {
|
|
25899
25947
|
if (re.lastIndex === idx) re.lastIndex++;
|
|
25900
25948
|
continue;
|
|
25901
25949
|
}
|
|
@@ -25991,7 +26039,117 @@ function scaToCanonical(input) {
|
|
|
25991
26039
|
|
|
25992
26040
|
// src/engine/version.ts
|
|
25993
26041
|
var ENGINE_VERSION = "0.1.0";
|
|
25994
|
-
var RULESET_VERSION = "2026.
|
|
26042
|
+
var RULESET_VERSION = "2026.10.20-23";
|
|
26043
|
+
|
|
26044
|
+
// src/engine/suppress.ts
|
|
26045
|
+
var SPHIORIGNORE_FILE = ".sphiorignore";
|
|
26046
|
+
function escapeRe(s) {
|
|
26047
|
+
return s.replace(/[.+^$(){}|[\]\\]/g, "\\$&");
|
|
26048
|
+
}
|
|
26049
|
+
function globToRegExp(glob) {
|
|
26050
|
+
let out2 = "";
|
|
26051
|
+
for (let i2 = 0; i2 < glob.length; i2++) {
|
|
26052
|
+
const c = glob[i2];
|
|
26053
|
+
if (c === "*") {
|
|
26054
|
+
if (glob[i2 + 1] === "*") {
|
|
26055
|
+
if (glob[i2 + 2] === "/") {
|
|
26056
|
+
out2 += "(?:[^/]+/)*";
|
|
26057
|
+
i2 += 2;
|
|
26058
|
+
} else {
|
|
26059
|
+
out2 += ".*";
|
|
26060
|
+
i2 += 1;
|
|
26061
|
+
}
|
|
26062
|
+
} else {
|
|
26063
|
+
out2 += "[^/]*";
|
|
26064
|
+
}
|
|
26065
|
+
continue;
|
|
26066
|
+
}
|
|
26067
|
+
if (c === "?") {
|
|
26068
|
+
out2 += "[^/]";
|
|
26069
|
+
continue;
|
|
26070
|
+
}
|
|
26071
|
+
out2 += escapeRe(c);
|
|
26072
|
+
}
|
|
26073
|
+
return new RegExp(`^${out2}$`);
|
|
26074
|
+
}
|
|
26075
|
+
function globMatches(glob, relPath) {
|
|
26076
|
+
const g = glob.endsWith("/") ? `${glob.slice(0, -1)}` : glob;
|
|
26077
|
+
if (globToRegExp(g).test(relPath)) return true;
|
|
26078
|
+
return globToRegExp(`${g}/**`).test(relPath);
|
|
26079
|
+
}
|
|
26080
|
+
function ruleMatches(pattern, ruleId) {
|
|
26081
|
+
if (pattern.endsWith("*")) return ruleId.startsWith(pattern.slice(0, -1));
|
|
26082
|
+
return pattern === ruleId;
|
|
26083
|
+
}
|
|
26084
|
+
function configSuppressionFor(cfg, path, ruleId) {
|
|
26085
|
+
return cfg.scoped.find((e) => e.ruleId && ruleMatches(e.ruleId, ruleId) && globMatches(e.glob, path));
|
|
26086
|
+
}
|
|
26087
|
+
var INLINE_RE = /sphior-ignore(-next-line)?(?:[ \t]+([A-Za-z][A-Za-z0-9_.-]*\*?))?(?:[ \t]*:[ \t]*(.*))?$/;
|
|
26088
|
+
function parseInlineSuppression(lineText) {
|
|
26089
|
+
const m = INLINE_RE.exec(lineText);
|
|
26090
|
+
if (!m) return void 0;
|
|
26091
|
+
return {
|
|
26092
|
+
nextLine: m[1] === "-next-line",
|
|
26093
|
+
ruleId: m[2],
|
|
26094
|
+
justification: m[3]?.trim() || void 0
|
|
26095
|
+
};
|
|
26096
|
+
}
|
|
26097
|
+
function inlineSuppressionFor(lines, startLine, ruleId) {
|
|
26098
|
+
const own = lines[startLine - 1] !== void 0 ? parseInlineSuppression(lines[startLine - 1]) : void 0;
|
|
26099
|
+
if (own && !own.nextLine && (!own.ruleId || ruleMatches(own.ruleId, ruleId))) return own;
|
|
26100
|
+
const prev = startLine >= 2 && lines[startLine - 2] !== void 0 ? parseInlineSuppression(lines[startLine - 2]) : void 0;
|
|
26101
|
+
if (prev && prev.nextLine && (!prev.ruleId || ruleMatches(prev.ruleId, ruleId))) return prev;
|
|
26102
|
+
return void 0;
|
|
26103
|
+
}
|
|
26104
|
+
function applySuppressions(findings, input = {}) {
|
|
26105
|
+
const cfg = input.config;
|
|
26106
|
+
const hasConfig = !!cfg && cfg.scoped.length > 0;
|
|
26107
|
+
const hasText = !!input.textByPath && input.textByPath.size > 0;
|
|
26108
|
+
if (!hasConfig && !hasText) return { active: findings, suppressed: [] };
|
|
26109
|
+
const linesCache = /* @__PURE__ */ new Map();
|
|
26110
|
+
const linesOf = (path) => {
|
|
26111
|
+
const cached = linesCache.get(path);
|
|
26112
|
+
if (cached) return cached;
|
|
26113
|
+
const text = input.textByPath?.get(path);
|
|
26114
|
+
if (text === void 0) return void 0;
|
|
26115
|
+
const lines = text.split(/\r?\n/);
|
|
26116
|
+
linesCache.set(path, lines);
|
|
26117
|
+
return lines;
|
|
26118
|
+
};
|
|
26119
|
+
const active = [];
|
|
26120
|
+
const suppressed = [];
|
|
26121
|
+
for (const f of findings) {
|
|
26122
|
+
const byConfig = hasConfig ? configSuppressionFor(cfg, f.location.path, f.rule_id) : void 0;
|
|
26123
|
+
if (byConfig) {
|
|
26124
|
+
suppressed.push({
|
|
26125
|
+
...f,
|
|
26126
|
+
suppression: {
|
|
26127
|
+
by: "config",
|
|
26128
|
+
rule_scoped: true,
|
|
26129
|
+
justification: byConfig.justification,
|
|
26130
|
+
source: `${SPHIORIGNORE_FILE}:${byConfig.line}`
|
|
26131
|
+
}
|
|
26132
|
+
});
|
|
26133
|
+
continue;
|
|
26134
|
+
}
|
|
26135
|
+
const lines = linesOf(f.location.path);
|
|
26136
|
+
const byInline = lines ? inlineSuppressionFor(lines, f.location.start_line, f.rule_id) : void 0;
|
|
26137
|
+
if (byInline) {
|
|
26138
|
+
suppressed.push({
|
|
26139
|
+
...f,
|
|
26140
|
+
suppression: {
|
|
26141
|
+
by: "inline",
|
|
26142
|
+
rule_scoped: !!byInline.ruleId,
|
|
26143
|
+
justification: byInline.justification,
|
|
26144
|
+
source: `${f.location.path}:${byInline.nextLine ? f.location.start_line - 1 : f.location.start_line}`
|
|
26145
|
+
}
|
|
26146
|
+
});
|
|
26147
|
+
continue;
|
|
26148
|
+
}
|
|
26149
|
+
active.push(f);
|
|
26150
|
+
}
|
|
26151
|
+
return { active, suppressed };
|
|
26152
|
+
}
|
|
25995
26153
|
|
|
25996
26154
|
// src/engine/run.ts
|
|
25997
26155
|
var kindOrder = {
|
|
@@ -26003,20 +26161,24 @@ var kindOrder = {
|
|
|
26003
26161
|
function runScan(input) {
|
|
26004
26162
|
const files = input.files ?? [];
|
|
26005
26163
|
const opts = { allowFingerprints: input.allowFingerprints, ignorePaths: input.ignorePaths };
|
|
26006
|
-
const
|
|
26164
|
+
const detected = [
|
|
26007
26165
|
...detectSecretsInFiles(files, opts),
|
|
26008
26166
|
...detectPatternsInFiles(files, opts),
|
|
26009
26167
|
...scaToCanonical(input.sca ?? [])
|
|
26010
26168
|
];
|
|
26011
|
-
|
|
26169
|
+
detected.sort((a, b) => {
|
|
26012
26170
|
if (a.location.path !== b.location.path) return a.location.path < b.location.path ? -1 : 1;
|
|
26013
26171
|
if (a.location.start_line !== b.location.start_line) return a.location.start_line - b.location.start_line;
|
|
26014
26172
|
if (a.location.start_col !== b.location.start_col) return a.location.start_col - b.location.start_col;
|
|
26015
26173
|
if (kindOrder[a.kind] !== kindOrder[b.kind]) return kindOrder[a.kind] - kindOrder[b.kind];
|
|
26016
26174
|
return a.rule_id < b.rule_id ? -1 : a.rule_id > b.rule_id ? 1 : 0;
|
|
26017
26175
|
});
|
|
26176
|
+
const { active: findings, suppressed } = applySuppressions(detected, {
|
|
26177
|
+
config: input.suppress,
|
|
26178
|
+
textByPath: new Map(files.map((f) => [f.path, f.text]))
|
|
26179
|
+
});
|
|
26018
26180
|
const sarif = toSarif(findings, { engineVersion: ENGINE_VERSION, rulesetVersion: RULESET_VERSION });
|
|
26019
|
-
return { findings, sarif, engine_version: ENGINE_VERSION, ruleset_version: RULESET_VERSION };
|
|
26181
|
+
return { findings, suppressed, sarif, engine_version: ENGINE_VERSION, ruleset_version: RULESET_VERSION };
|
|
26020
26182
|
}
|
|
26021
26183
|
|
|
26022
26184
|
// src/engine/mcp/fix-context.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sphior/scan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
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": {
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
],
|
|
13
13
|
"repository": {
|
|
14
14
|
"type": "git",
|
|
15
|
-
"url": "git+https://github.com/Toraastrta/
|
|
16
|
-
"directory": "
|
|
15
|
+
"url": "git+https://github.com/Toraastrta/sphior-code.git",
|
|
16
|
+
"directory": "packages/sphior-scan"
|
|
17
17
|
},
|
|
18
18
|
"homepage": "https://sphior.com/code",
|
|
19
19
|
"publishConfig": {
|
|
@@ -55,5 +55,8 @@
|
|
|
55
55
|
"esbuild": "^0.24.0",
|
|
56
56
|
"typescript": "^5.5.0"
|
|
57
57
|
},
|
|
58
|
-
"license": "MIT"
|
|
58
|
+
"license": "MIT",
|
|
59
|
+
"bugs": {
|
|
60
|
+
"url": "https://github.com/Toraastrta/sphior-code/issues"
|
|
61
|
+
}
|
|
59
62
|
}
|