@yottameta/yotta-chain 0.1.2 → 0.1.5
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/CHANGELOG.md +15 -0
- package/LICENSE +20 -20
- package/README.md +7 -5
- package/README.zh-CN.md +7 -5
- package/SKILL.md +9 -7
- package/bin/install.js +336 -163
- package/install.sh +327 -132
- package/package.json +5 -2
- package/references/rules.md +10 -5
- package/scripts/yotta_chain.py +307 -24
- package/scripts/test_yotta_chain.py +0 -616
package/scripts/yotta_chain.py
CHANGED
|
@@ -32,10 +32,11 @@ from pathlib import Path
|
|
|
32
32
|
|
|
33
33
|
try:
|
|
34
34
|
sys.stdout.reconfigure(encoding="utf-8")
|
|
35
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
35
36
|
except Exception:
|
|
36
37
|
pass
|
|
37
38
|
|
|
38
|
-
VERSION = "0.1.
|
|
39
|
+
VERSION = "0.1.5"
|
|
39
40
|
|
|
40
41
|
SEVERITY_ORDER = ["info", "low", "medium", "high"]
|
|
41
42
|
SEVERITY_RANK = {s: i for i, s in enumerate(SEVERITY_ORDER)}
|
|
@@ -817,6 +818,187 @@ def _v1_name(key):
|
|
|
817
818
|
return key.split("@")[0]
|
|
818
819
|
|
|
819
820
|
|
|
821
|
+
def _descriptor_name_version(descriptor):
|
|
822
|
+
"""Split an npm descriptor such as @scope/pkg@1.2.3 or lodash@^4."""
|
|
823
|
+
s = str(descriptor or "").strip().strip("\"'")
|
|
824
|
+
if not s:
|
|
825
|
+
return None, None
|
|
826
|
+
if s.startswith("@"):
|
|
827
|
+
slash = s.find("/")
|
|
828
|
+
at = s.find("@", slash + 1) if slash >= 0 else -1
|
|
829
|
+
if at > slash:
|
|
830
|
+
return s[:at], s[at + 1:].split("(", 1)[0]
|
|
831
|
+
at = s.rfind("@")
|
|
832
|
+
if at > 0:
|
|
833
|
+
return s[:at], s[at + 1:].split("(", 1)[0]
|
|
834
|
+
return s, None
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
def _jsonc_load(text):
|
|
838
|
+
"""Parse the JSONC-like bun text lockfile without third-party modules."""
|
|
839
|
+
cleaned = re.sub(r"(?m)^\s*//.*$", "", text or "")
|
|
840
|
+
cleaned = re.sub(r",(\s*[}\]])", r"\1", cleaned)
|
|
841
|
+
return json.loads(cleaned)
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
def parse_yarn_lock(text):
|
|
845
|
+
"""Parse the name/version/resolved/integrity subset of yarn.lock."""
|
|
846
|
+
packages = {}
|
|
847
|
+
current = None
|
|
848
|
+
for raw in (text or "").splitlines():
|
|
849
|
+
if raw and not raw.startswith((" ", "\t", "#")) and raw.rstrip().endswith(":"):
|
|
850
|
+
descriptor_text = raw.rstrip()[:-1]
|
|
851
|
+
current = None
|
|
852
|
+
for item in descriptor_text.split(","):
|
|
853
|
+
name, _ = _descriptor_name_version(item)
|
|
854
|
+
if not name:
|
|
855
|
+
continue
|
|
856
|
+
current = name
|
|
857
|
+
packages.setdefault(name, [])
|
|
858
|
+
break
|
|
859
|
+
continue
|
|
860
|
+
if current is None:
|
|
861
|
+
continue
|
|
862
|
+
m = re.match(r"^\s+version\s+\"?([^\"\s]+)\"?\s*$", raw)
|
|
863
|
+
if m:
|
|
864
|
+
entry = {"version": m.group(1), "resolved": None, "integrity": None,
|
|
865
|
+
"dev": False, "optional": False, "deps": {}, "key": current}
|
|
866
|
+
packages.setdefault(current, []).append(entry)
|
|
867
|
+
continue
|
|
868
|
+
if not packages.get(current):
|
|
869
|
+
continue
|
|
870
|
+
entry = packages[current][-1]
|
|
871
|
+
m = re.match(r"^\s+resolved\s+\"?([^\"\s]+)\"?\s*$", raw)
|
|
872
|
+
if m:
|
|
873
|
+
entry["resolved"] = m.group(1)
|
|
874
|
+
continue
|
|
875
|
+
m = re.match(r"^\s+integrity\s+\"?([^\"\s]+)\"?\s*$", raw)
|
|
876
|
+
if m:
|
|
877
|
+
entry["integrity"] = m.group(1)
|
|
878
|
+
return {"lockfileVersion": None, "root": {}, "packages": packages}
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
def _pnpm_key_name_version(key):
|
|
882
|
+
s = str(key or "").strip().strip("\"'").split("(", 1)[0]
|
|
883
|
+
if not s:
|
|
884
|
+
return None, None
|
|
885
|
+
if s.startswith("/"):
|
|
886
|
+
s = s[1:]
|
|
887
|
+
if s.startswith("@"):
|
|
888
|
+
slash = s.find("/")
|
|
889
|
+
at = s.rfind("@")
|
|
890
|
+
if slash >= 0 and at > slash:
|
|
891
|
+
return s[:at], s[at + 1:]
|
|
892
|
+
if slash >= 0:
|
|
893
|
+
name, _, version = s.rpartition("/")
|
|
894
|
+
return (name, version) if version else (None, None)
|
|
895
|
+
return None, None
|
|
896
|
+
if "@" in s:
|
|
897
|
+
name, _, version = s.rpartition("@")
|
|
898
|
+
return (name, version) if name and version else (None, None)
|
|
899
|
+
if "/" in s:
|
|
900
|
+
name, _, version = s.rpartition("/")
|
|
901
|
+
return (name, version) if name and version else (None, None)
|
|
902
|
+
return None, None
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
def parse_pnpm_lock(text):
|
|
906
|
+
"""Parse common pnpm-lock.yaml package keys without a YAML dependency."""
|
|
907
|
+
packages = {}
|
|
908
|
+
current = None
|
|
909
|
+
in_packages = False
|
|
910
|
+
lock_version = None
|
|
911
|
+
for raw in (text or "").splitlines():
|
|
912
|
+
m = re.match(r"^lockfileVersion:\s*['\"]?([^'\"\s]+)", raw)
|
|
913
|
+
if m:
|
|
914
|
+
try:
|
|
915
|
+
lock_version = int(float(m.group(1)))
|
|
916
|
+
except ValueError:
|
|
917
|
+
lock_version = m.group(1)
|
|
918
|
+
continue
|
|
919
|
+
if re.match(r"^packages:\s*$", raw):
|
|
920
|
+
in_packages = True
|
|
921
|
+
continue
|
|
922
|
+
if in_packages and raw and not raw.startswith((" ", "\t")):
|
|
923
|
+
in_packages = False
|
|
924
|
+
current = None
|
|
925
|
+
continue
|
|
926
|
+
if not in_packages:
|
|
927
|
+
continue
|
|
928
|
+
m = re.match(r"^\s{2}(['\"]?)(.+?)\1:\s*$", raw)
|
|
929
|
+
if m:
|
|
930
|
+
name, version = _pnpm_key_name_version(m.group(2))
|
|
931
|
+
if name and version and version[:1].isdigit():
|
|
932
|
+
current = name
|
|
933
|
+
packages.setdefault(name, []).append({
|
|
934
|
+
"version": version, "resolved": None, "integrity": None,
|
|
935
|
+
"dev": False, "optional": False, "deps": {}, "key": m.group(2),
|
|
936
|
+
})
|
|
937
|
+
else:
|
|
938
|
+
current = None
|
|
939
|
+
continue
|
|
940
|
+
if current and packages.get(current):
|
|
941
|
+
m = re.search(r"integrity:\s*([^,}\s]+)", raw)
|
|
942
|
+
if m:
|
|
943
|
+
packages[current][-1]["integrity"] = m.group(1).strip("\"'")
|
|
944
|
+
return {"lockfileVersion": lock_version, "root": {}, "packages": packages}
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
def parse_bun_lock(text):
|
|
948
|
+
"""Parse bun.lock (JSONC-like text). bun.lockb is intentionally unsupported."""
|
|
949
|
+
try:
|
|
950
|
+
data = _jsonc_load(text)
|
|
951
|
+
except Exception:
|
|
952
|
+
return None
|
|
953
|
+
if not isinstance(data, dict):
|
|
954
|
+
return None
|
|
955
|
+
packages = {}
|
|
956
|
+
for key, ent in (data.get("packages") or {}).items():
|
|
957
|
+
name = None
|
|
958
|
+
version = None
|
|
959
|
+
integrity = None
|
|
960
|
+
if isinstance(ent, list) and ent:
|
|
961
|
+
name, version = _descriptor_name_version(ent[0])
|
|
962
|
+
integrity = ent[3] if len(ent) > 3 else None
|
|
963
|
+
elif isinstance(ent, dict):
|
|
964
|
+
name = ent.get("name") or key
|
|
965
|
+
version = ent.get("version")
|
|
966
|
+
integrity = ent.get("integrity")
|
|
967
|
+
if not name:
|
|
968
|
+
name = key
|
|
969
|
+
packages.setdefault(name, []).append({
|
|
970
|
+
"version": str(version) if version else None,
|
|
971
|
+
"resolved": None, "integrity": integrity,
|
|
972
|
+
"dev": False, "optional": False, "deps": {}, "key": key,
|
|
973
|
+
})
|
|
974
|
+
workspace = (data.get("workspaces") or {}).get("") or {}
|
|
975
|
+
root = {
|
|
976
|
+
"name": workspace.get("name") or data.get("name"),
|
|
977
|
+
"version": workspace.get("version") or data.get("version"),
|
|
978
|
+
}
|
|
979
|
+
return {"lockfileVersion": data.get("lockfileVersion"), "root": root, "packages": packages}
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
def _select_npm_lockfile(base, pj):
|
|
983
|
+
manager = str(pj.get("packageManager") or "").split("@", 1)[0].lower()
|
|
984
|
+
by_manager = {
|
|
985
|
+
"npm": ("package-lock.json", "npm-shrinkwrap.json"),
|
|
986
|
+
"yarn": ("yarn.lock",),
|
|
987
|
+
"pnpm": ("pnpm-lock.yaml",),
|
|
988
|
+
"bun": ("bun.lock", "bun.lockb"),
|
|
989
|
+
}
|
|
990
|
+
candidates = list(by_manager.get(manager, ()))
|
|
991
|
+
for name in ("package-lock.json", "npm-shrinkwrap.json", "yarn.lock",
|
|
992
|
+
"pnpm-lock.yaml", "bun.lock", "bun.lockb"):
|
|
993
|
+
if name not in candidates:
|
|
994
|
+
candidates.append(name)
|
|
995
|
+
for name in candidates:
|
|
996
|
+
path = base / name
|
|
997
|
+
if path.is_file():
|
|
998
|
+
return path
|
|
999
|
+
return None
|
|
1000
|
+
|
|
1001
|
+
|
|
820
1002
|
def parse_package_lock(text):
|
|
821
1003
|
"""Parse package-lock.json / npm-shrinkwrap.json.
|
|
822
1004
|
|
|
@@ -894,27 +1076,36 @@ def check_npm(project_dir, findings, sbom_pkgs):
|
|
|
894
1076
|
if isinstance(pub_cfg, dict) and pub_cfg.get("registry"):
|
|
895
1077
|
npmrc["registry"] = npmrc["registry"] or pub_cfg["registry"]
|
|
896
1078
|
|
|
897
|
-
lock_path =
|
|
898
|
-
for cand in ("package-lock.json", "npm-shrinkwrap.json"):
|
|
899
|
-
if (base / cand).exists():
|
|
900
|
-
lock_path = base / cand
|
|
901
|
-
break
|
|
902
|
-
|
|
1079
|
+
lock_path = _select_npm_lockfile(base, pj)
|
|
903
1080
|
if lock_path is None:
|
|
904
1081
|
if manifest_deps:
|
|
905
1082
|
findings.append(Finding(
|
|
906
1083
|
"missing_lockfile", "medium", "package.json", None,
|
|
907
|
-
"
|
|
908
|
-
"
|
|
1084
|
+
"缺少锁文件(支持 package-lock.json / npm-shrinkwrap.json / yarn.lock / pnpm-lock.yaml / bun.lock)",
|
|
1085
|
+
"依赖版本未锁定,安装结果不可复现;建议按项目包管理器提交对应锁文件", ecosystem="npm"))
|
|
909
1086
|
else:
|
|
910
|
-
lock =
|
|
911
|
-
if
|
|
1087
|
+
lock = None
|
|
1088
|
+
if lock_path.name == "bun.lockb":
|
|
1089
|
+
findings.append(Finding(
|
|
1090
|
+
"lockfile_parse_unsupported", "info", lock_path.name, None,
|
|
1091
|
+
"检测到 bun.lockb,但二进制锁文件本版本不做深度解析",
|
|
1092
|
+
"已按锁文件存在处理,不误报 missing_lockfile;需要深度一致性校验时请改用 bun.lock 文本锁文件", ecosystem="npm"))
|
|
1093
|
+
elif lock_path.name in ("package-lock.json", "npm-shrinkwrap.json"):
|
|
1094
|
+
lock = parse_package_lock(_read_text(lock_path))
|
|
1095
|
+
elif lock_path.name == "yarn.lock":
|
|
1096
|
+
lock = parse_yarn_lock(_read_text(lock_path))
|
|
1097
|
+
elif lock_path.name == "pnpm-lock.yaml":
|
|
1098
|
+
lock = parse_pnpm_lock(_read_text(lock_path))
|
|
1099
|
+
elif lock_path.name == "bun.lock":
|
|
1100
|
+
lock = parse_bun_lock(_read_text(lock_path))
|
|
1101
|
+
if lock is None and lock_path.name != "bun.lockb":
|
|
912
1102
|
findings.append(Finding(
|
|
913
1103
|
"lockfile_parse_error", "medium", lock_path.name, None,
|
|
914
|
-
"
|
|
915
|
-
"
|
|
1104
|
+
"锁文件解析失败(结构异常或格式不受支持)",
|
|
1105
|
+
"请用对应包管理器重新生成锁文件", ecosystem="npm"))
|
|
916
1106
|
else:
|
|
917
|
-
|
|
1107
|
+
if lock is not None:
|
|
1108
|
+
_check_npm_lock(base, lock_path, lock, pj, manifest_deps, npmrc, findings, sbom_pkgs)
|
|
918
1109
|
|
|
919
1110
|
for name, info in manifest_deps.items():
|
|
920
1111
|
rng = (info["range"] or "").strip()
|
|
@@ -1242,17 +1433,27 @@ def check_python(project_dir, findings, sbom_pkgs):
|
|
|
1242
1433
|
py = parse_toml(_read_text(pp_path))
|
|
1243
1434
|
proj_deps, poetry_deps = pyproject_deps(py)
|
|
1244
1435
|
declared = proj_deps + poetry_deps
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
if
|
|
1436
|
+
poetry_lock_path = base / "poetry.lock"
|
|
1437
|
+
uv_lock_path = base / "uv.lock"
|
|
1438
|
+
lock_path = None
|
|
1439
|
+
lock_kind = None
|
|
1440
|
+
if poetry_deps and poetry_lock_path.exists():
|
|
1441
|
+
lock_path, lock_kind = poetry_lock_path, "poetry"
|
|
1442
|
+
elif uv_lock_path.exists():
|
|
1443
|
+
lock_path, lock_kind = uv_lock_path, "uv"
|
|
1444
|
+
elif poetry_lock_path.exists():
|
|
1445
|
+
lock_path, lock_kind = poetry_lock_path, "poetry"
|
|
1446
|
+
lock = parse_toml(_read_text(lock_path)) if lock_path is not None else None
|
|
1447
|
+
if declared and lock_path is None:
|
|
1250
1448
|
findings.append(Finding(
|
|
1251
1449
|
"missing_lockfile", "medium", "pyproject.toml", None,
|
|
1252
|
-
"pyproject.toml 声明了 %d 个依赖但没有 poetry.lock" % len(declared),
|
|
1253
|
-
"
|
|
1450
|
+
"pyproject.toml 声明了 %d 个依赖但没有 poetry.lock / uv.lock" % len(declared),
|
|
1451
|
+
"依赖版本未锁定,安装结果不可复现;请按所用工具提交 poetry.lock 或 uv.lock", ecosystem="python"))
|
|
1254
1452
|
elif lock is not None:
|
|
1255
|
-
|
|
1453
|
+
if lock_kind == "poetry":
|
|
1454
|
+
_check_poetry_lock(lock, declared, findings, sbom_pkgs)
|
|
1455
|
+
else:
|
|
1456
|
+
_check_uv_lock(lock, declared, findings, sbom_pkgs)
|
|
1256
1457
|
for src in poetry_sources(py):
|
|
1257
1458
|
url = src.get("url")
|
|
1258
1459
|
if not url:
|
|
@@ -1325,6 +1526,59 @@ def _check_poetry_lock(lock, declared, findings, sbom_pkgs):
|
|
|
1325
1526
|
})
|
|
1326
1527
|
|
|
1327
1528
|
|
|
1529
|
+
def _python_dep_name(spec):
|
|
1530
|
+
s = str(spec or "").strip().strip("\"'")
|
|
1531
|
+
if " @ " in s:
|
|
1532
|
+
return s.split(" @ ", 1)[0].strip()
|
|
1533
|
+
if ";" in s:
|
|
1534
|
+
s = s.split(";", 1)[0].strip()
|
|
1535
|
+
return re.split(r"[<>=!~\[\s]", s, 1)[0].strip()
|
|
1536
|
+
|
|
1537
|
+
|
|
1538
|
+
def _check_uv_lock(lock, declared, findings, sbom_pkgs):
|
|
1539
|
+
pkgs = {}
|
|
1540
|
+
for p in (lock.get("package") or []):
|
|
1541
|
+
if isinstance(p, dict) and p.get("name"):
|
|
1542
|
+
pkgs[p["name"]] = p
|
|
1543
|
+
for name, spec in declared:
|
|
1544
|
+
p = pkgs.get(name)
|
|
1545
|
+
if p is None:
|
|
1546
|
+
findings.append(Finding(
|
|
1547
|
+
"lockfile_missing_entry", "high", "uv.lock", name,
|
|
1548
|
+
"pyproject.toml 声明了 %s(%s),但 uv.lock 中没有该包" % (name, spec),
|
|
1549
|
+
"锁文件过期或手工改动,运行 uv lock 重新生成", ecosystem="python"))
|
|
1550
|
+
continue
|
|
1551
|
+
ver = str(p.get("version") or "")
|
|
1552
|
+
if ver and spec not in ("", "*") and not pep440_satisfies(ver, spec):
|
|
1553
|
+
findings.append(Finding(
|
|
1554
|
+
"lockfile_range_unsatisfied", "high", "uv.lock", name,
|
|
1555
|
+
"uv.lock 中 %s=%s 不满足 pyproject.toml 声明 %s" % (name, ver, spec),
|
|
1556
|
+
"声明与锁定不一致,安装可能拉取意外版本", ecosystem="python"))
|
|
1557
|
+
for name, p in pkgs.items():
|
|
1558
|
+
deps = p.get("dependencies") or []
|
|
1559
|
+
dep_names = []
|
|
1560
|
+
if isinstance(deps, dict):
|
|
1561
|
+
dep_names = list(deps.keys())
|
|
1562
|
+
elif isinstance(deps, list):
|
|
1563
|
+
dep_names = [_python_dep_name(x) for x in deps if _python_dep_name(x)]
|
|
1564
|
+
for dn in dep_names:
|
|
1565
|
+
if dn not in pkgs:
|
|
1566
|
+
findings.append(Finding(
|
|
1567
|
+
"lockfile_dangling_ref", "high", "uv.lock", name,
|
|
1568
|
+
"uv.lock 里 %s 依赖的 %s 不存在于锁文件包列表" % (name, dn),
|
|
1569
|
+
"依赖图断裂,安装可能失败或行为异常", ecosystem="python"))
|
|
1570
|
+
sbom_pkgs.append({
|
|
1571
|
+
"ecosystem": "python",
|
|
1572
|
+
"name": name,
|
|
1573
|
+
"version": str(p.get("version") or ""),
|
|
1574
|
+
"resolved": "",
|
|
1575
|
+
"integrity": "",
|
|
1576
|
+
"scope": "optional" if p.get("optional") else "required",
|
|
1577
|
+
"direct": name in dict(declared),
|
|
1578
|
+
"deps": dep_names,
|
|
1579
|
+
})
|
|
1580
|
+
|
|
1581
|
+
|
|
1328
1582
|
def check_pipfile(project_dir, findings, sbom_pkgs):
|
|
1329
1583
|
base = Path(project_dir)
|
|
1330
1584
|
pf = base / "Pipfile"
|
|
@@ -1654,7 +1908,30 @@ def _csv_escape(v):
|
|
|
1654
1908
|
return s
|
|
1655
1909
|
|
|
1656
1910
|
|
|
1657
|
-
def
|
|
1911
|
+
def _scanned_files(base, eco):
|
|
1912
|
+
"""Return the input files relevant to the detected ecosystems."""
|
|
1913
|
+
out = []
|
|
1914
|
+
if "npm" in eco:
|
|
1915
|
+
out.append("package.json")
|
|
1916
|
+
pj = parse_package_json(_read_text(base / "package.json")) or {}
|
|
1917
|
+
lock = _select_npm_lockfile(base, pj)
|
|
1918
|
+
if lock is not None:
|
|
1919
|
+
out.append(lock.name)
|
|
1920
|
+
if (base / ".npmrc").is_file():
|
|
1921
|
+
out.append(".npmrc")
|
|
1922
|
+
if "python" in eco:
|
|
1923
|
+
for path in sorted(base.glob("requirements*.txt")):
|
|
1924
|
+
out.append(path.name)
|
|
1925
|
+
for name in ("pyproject.toml", "poetry.lock", "uv.lock",
|
|
1926
|
+
"Pipfile", "Pipfile.lock"):
|
|
1927
|
+
if (base / name).is_file():
|
|
1928
|
+
out.append(name)
|
|
1929
|
+
if "maven" in eco and (base / "pom.xml").is_file():
|
|
1930
|
+
out.append("pom.xml")
|
|
1931
|
+
return sorted(set(out))
|
|
1932
|
+
|
|
1933
|
+
|
|
1934
|
+
def _collect(project_dir, findings, sbom_pkgs, root_component, scanned_files=None):
|
|
1658
1935
|
"""Run all ecosystem checks; returns sorted ecosystem list."""
|
|
1659
1936
|
base = Path(project_dir)
|
|
1660
1937
|
eco = []
|
|
@@ -1671,6 +1948,8 @@ def _collect(project_dir, findings, sbom_pkgs, root_component):
|
|
|
1671
1948
|
root_component.update({"ecosystem": "python", "name": proj["name"], "version": str(proj.get("version") or "")})
|
|
1672
1949
|
if check_maven(project_dir, findings, sbom_pkgs):
|
|
1673
1950
|
eco.append("maven")
|
|
1951
|
+
if scanned_files is not None:
|
|
1952
|
+
scanned_files.extend(_scanned_files(base, eco))
|
|
1674
1953
|
return eco
|
|
1675
1954
|
|
|
1676
1955
|
|
|
@@ -1682,7 +1961,8 @@ def cmd_scan(args):
|
|
|
1682
1961
|
findings = []
|
|
1683
1962
|
sbom_pkgs = []
|
|
1684
1963
|
root_component = {}
|
|
1685
|
-
|
|
1964
|
+
scanned_files = []
|
|
1965
|
+
eco = _collect(args.path, findings, sbom_pkgs, root_component, scanned_files=scanned_files)
|
|
1686
1966
|
if not eco:
|
|
1687
1967
|
print("错误:%s 下未发现支持的依赖清单/锁文件(package.json / requirements*.txt / pyproject.toml / Pipfile / pom.xml)" % base, file=sys.stderr)
|
|
1688
1968
|
return 4
|
|
@@ -1699,6 +1979,7 @@ def cmd_scan(args):
|
|
|
1699
1979
|
"version": VERSION,
|
|
1700
1980
|
"project": str(base),
|
|
1701
1981
|
"ecosystems": eco,
|
|
1982
|
+
"scannedFiles": sorted(set(scanned_files)),
|
|
1702
1983
|
"files": sorted({f.file for f in findings}),
|
|
1703
1984
|
"summary": {s: sum(1 for f in findings if f.severity == s) for s in SEVERITY_ORDER},
|
|
1704
1985
|
"findings": [f.to_dict() for f in shown],
|
|
@@ -1714,6 +1995,8 @@ def cmd_scan(args):
|
|
|
1714
1995
|
else:
|
|
1715
1996
|
lines = ["元链 yotta-chain %s — 供应链依赖校验" % VERSION]
|
|
1716
1997
|
lines.append("项目:%s 生态:%s" % (base, ", ".join(eco)))
|
|
1998
|
+
if scanned_files:
|
|
1999
|
+
lines.append("输入文件:%s" % ", ".join(sorted(set(scanned_files))))
|
|
1717
2000
|
if not shown:
|
|
1718
2001
|
lines.append("未发现 %s 及以上风险项" % args.level)
|
|
1719
2002
|
for s in SEVERITY_ORDER:
|