@testzugang/pi-plugin-dependency-audit 0.1.0

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.
@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+
10
+ def parse_args() -> argparse.Namespace:
11
+ parser = argparse.ArgumentParser(description="Summarize aggregated pi dependency audit JSON as Markdown.")
12
+ parser.add_argument("--input", default="/tmp/pi_audit_aggregated.json", help="Input JSON from run_pi_dependency_audit.py")
13
+ parser.add_argument("--output", default="/tmp/pi_audit_report.md", help="Output markdown report path")
14
+ return parser.parse_args()
15
+
16
+
17
+ def decision_label(decision: str) -> str:
18
+ labels = {
19
+ "QUARANTINE": "❌ QUARANTINE",
20
+ "BLOCK_UNTIL_REVIEW": "⚠️ BLOCK UNTIL REVIEW",
21
+ "REVIEW_BEFORE_USE": "🟡 REVIEW BEFORE USE",
22
+ "PASS_WITH_CAUTION": "✅ PASS WITH CAUTION",
23
+ "PASS_UP_TO_DATE": "✅ UP TO DATE",
24
+ "SKIP_NOT_INSTALLED": "➖ NOT INSTALLED",
25
+ "SKIP_MISSING": "➖ MISSING",
26
+ "SKIP_NO_REMOTE_BRANCH": "➖ NO REMOTE BRANCH",
27
+ "SKIP_TOO_FRESH": "⏱️ TOO FRESH",
28
+ }
29
+ return labels.get(decision, decision)
30
+
31
+
32
+ def short_rev(value: Any) -> str:
33
+ text = str(value or "-")
34
+ return text[:8] if len(text) > 8 and all(c in "0123456789abcdef" for c in text.lower()) else text
35
+
36
+
37
+ def sev(counts: dict[str, Any], key: str) -> int:
38
+ raw = counts.get(key, 0)
39
+ return raw if isinstance(raw, int) else 0
40
+
41
+
42
+ def is_transitive_node_modules_finding(finding: dict[str, Any]) -> bool:
43
+ path = str(finding.get("path", ""))
44
+ return "node_modules/" in path
45
+
46
+
47
+ def significant_findings(findings: list[dict[str, Any]]) -> list[dict[str, Any]]:
48
+ return [f for f in findings if str(f.get("severity")) in {"CRITICAL", "HIGH", "MEDIUM"}]
49
+
50
+
51
+ def render_markdown(results: list[dict[str, Any]]) -> str:
52
+ md: list[str] = []
53
+ md.append("# 🛡️ Global Pi Dependency Security Audit Report")
54
+ md.append("")
55
+ md.append("## 📊 Executive Summary")
56
+ md.append("")
57
+ md.append("| Target | Type | Current | Latest | Status | Decision | C | H | M | L | I |")
58
+ md.append("| :--- | :---: | :--- | :--- | :--- | :--- | ---: | ---: | ---: | ---: | ---: |")
59
+
60
+ updates: list[dict[str, Any]] = []
61
+ errors: list[dict[str, Any]] = []
62
+ deferred: list[dict[str, Any]] = []
63
+
64
+ for item in results:
65
+ status = str(item.get("status", "unknown"))
66
+ decision = str(item.get("decision", "UNKNOWN"))
67
+ counts = item.get("counts", {}) if isinstance(item.get("counts"), dict) else {}
68
+
69
+ if status == "error":
70
+ errors.append(item)
71
+
72
+ if status == "update_available":
73
+ updates.append(item)
74
+ if status == "too_fresh":
75
+ deferred.append(item)
76
+
77
+ md.append(
78
+ "| `{name}` | {typ} | `{current}` | `{latest}` | {status} | {decision} | {c} | {h} | {m} | {l} | {i} |".format(
79
+ name=item.get("name", "?"),
80
+ typ=item.get("type", "?"),
81
+ current=short_rev(item.get("current")),
82
+ latest=short_rev(item.get("latest")),
83
+ status=status,
84
+ decision=decision_label(decision),
85
+ c=sev(counts, "CRITICAL"),
86
+ h=sev(counts, "HIGH"),
87
+ m=sev(counts, "MEDIUM"),
88
+ l=sev(counts, "LOW"),
89
+ i=sev(counts, "INFO"),
90
+ )
91
+ )
92
+
93
+ if errors:
94
+ md.append("")
95
+ md.append("## ⚠️ Errors")
96
+ md.append("")
97
+ for item in errors:
98
+ md.append(f"- **{item.get('name', '?')}**: {item.get('error', 'unknown error')}")
99
+
100
+ md.append("")
101
+ md.append("## 🔍 Findings for Pending Updates")
102
+ md.append("")
103
+ if not updates:
104
+ md.append("No pending updates were detected.")
105
+ else:
106
+ for item in updates:
107
+ md.append(
108
+ f"### `{item.get('name','?')}` ({short_rev(item.get('current'))} ➜ {short_rev(item.get('latest'))})"
109
+ )
110
+ md.append(f"- **Decision:** {decision_label(str(item.get('decision', 'UNKNOWN')))}")
111
+
112
+ findings = item.get("findings", []) if isinstance(item.get("findings"), list) else []
113
+ significant = significant_findings(findings)
114
+ transitive_significant = [f for f in significant if is_transitive_node_modules_finding(f)]
115
+ first_party_significant = [f for f in significant if not is_transitive_node_modules_finding(f)]
116
+
117
+ if not findings:
118
+ md.append("- No findings.")
119
+ elif not significant:
120
+ md.append(f"- Only LOW/INFO findings ({len(findings)} total).")
121
+ else:
122
+ md.append(
123
+ f"- Significant findings: {len(significant)} total (first-party: {len(first_party_significant)}, transitive node_modules: {len(transitive_significant)})."
124
+ )
125
+
126
+ if first_party_significant:
127
+ md.append("- First-party findings:")
128
+ for finding in first_party_significant[:12]:
129
+ line = finding.get("line")
130
+ line_suffix = f":{line}" if line else ""
131
+ md.append(
132
+ f" - [{finding.get('severity','?')}] {finding.get('title','?')} — `{finding.get('path','?')}{line_suffix}`"
133
+ )
134
+ recommendation = finding.get("recommendation")
135
+ if recommendation:
136
+ md.append(f" - Recommendation: {recommendation}")
137
+ if len(first_party_significant) > 12:
138
+ md.append(f" - … {len(first_party_significant) - 12} more first-party significant finding(s)")
139
+
140
+ if transitive_significant:
141
+ md.append("- Transitive node_modules findings (summarized):")
142
+ by_severity: dict[str, int] = {}
143
+ by_category: dict[str, int] = {}
144
+ for finding in transitive_significant:
145
+ sev_key = str(finding.get("severity", "?"))
146
+ cat_key = str(finding.get("category", "?"))
147
+ by_severity[sev_key] = by_severity.get(sev_key, 0) + 1
148
+ by_category[cat_key] = by_category.get(cat_key, 0) + 1
149
+ sev_summary = ", ".join(f"{k}: {by_severity[k]}" for k in sorted(by_severity))
150
+ cat_summary = ", ".join(
151
+ f"{k}: {v}" for k, v in sorted(by_category.items(), key=lambda kv: (-kv[1], kv[0]))[:8]
152
+ )
153
+ md.append(f" - Severity breakdown: {sev_summary}")
154
+ md.append(f" - Top categories: {cat_summary}")
155
+ md.append("")
156
+
157
+ safe = [i for i in updates if str(i.get("decision")) in {"PASS_WITH_CAUTION", "PASS_UP_TO_DATE"}]
158
+ review = [i for i in updates if str(i.get("decision")) == "REVIEW_BEFORE_USE"]
159
+ blocked = [i for i in updates if str(i.get("decision")) in {"BLOCK_UNTIL_REVIEW", "QUARANTINE"}]
160
+
161
+ def get_pi_source(item: dict[str, Any]) -> str:
162
+ if item.get("type") == "npm":
163
+ return f"npm:{item['name']}"
164
+ source = item.get("source")
165
+ if source:
166
+ return str(source)
167
+ name = item.get("name", "")
168
+ if name == "pi-skills":
169
+ return "git:github.com/fgladisch/pi-skills"
170
+ if name == "pi-plugins":
171
+ return "git:github.com/testzugang/pi-plugins"
172
+ if name == "pi-community-themes":
173
+ return "git:https://github.com/hasit/pi-community-themes"
174
+ return f"git:github.com/testzugang/{name}"
175
+
176
+ allowed_updates = [i for i in updates if str(i.get("decision")) not in {"BLOCK_UNTIL_REVIEW", "QUARANTINE"}]
177
+
178
+ if deferred:
179
+ md.append("## ⏱️ Deferred by Minimum Update Age")
180
+ md.append("")
181
+ for item in deferred:
182
+ age = item.get("update_age_hours")
183
+ threshold = item.get("min_update_age_hours")
184
+ age_str = f"{age:.1f}h" if isinstance(age, (int, float)) else "unknown"
185
+ threshold_str = f"{threshold:.1f}h" if isinstance(threshold, (int, float)) else "unknown"
186
+ md.append(
187
+ f"- `{item.get('name','?')}` ({short_rev(item.get('current'))} ➜ {short_rev(item.get('latest'))}) — {age_str} < {threshold_str}"
188
+ )
189
+ md.append("")
190
+
191
+ md.append("## 💡 Recommendation")
192
+ md.append("")
193
+ md.append(f"- Safe to update now: **{len(safe)}**")
194
+ md.append(f"- Needs manual review: **{len(review)}**")
195
+ md.append(f"- Blocked/quarantine: **{len(blocked)}**")
196
+ md.append(f"- Deferred by age gate: **{len(deferred)}**")
197
+
198
+ if allowed_updates:
199
+ md.append("")
200
+ md.append("### 🚀 Suggested Update Command")
201
+ md.append("")
202
+
203
+ # Check if we are skipping anything (blocked or deferred)
204
+ has_blocked = len(blocked) > 0
205
+ has_deferred = len(deferred) > 0
206
+
207
+ if has_blocked or has_deferred:
208
+ md.append("Since some updates are blocked, quarantined, or too fresh, run the following selective updates:")
209
+ md.append("```bash")
210
+ cmd_lines = []
211
+ for item in allowed_updates:
212
+ cmd_lines.append(f"pi update {get_pi_source(item)}")
213
+ md.append(" && \\\n".join(cmd_lines))
214
+ md.append("```")
215
+ else:
216
+ md.append("Since all pending updates are safe and approved, you can update everything at once:")
217
+ md.append("```bash")
218
+ md.append("pi update --extensions")
219
+ md.append("```")
220
+
221
+ return "\n".join(md) + "\n"
222
+
223
+
224
+ def main() -> int:
225
+ args = parse_args()
226
+ input_path = Path(args.input)
227
+ output_path = Path(args.output)
228
+
229
+ if not input_path.exists():
230
+ raise SystemExit(f"Input file not found: {input_path}")
231
+
232
+ results = json.loads(input_path.read_text(encoding="utf-8"))
233
+ if not isinstance(results, list):
234
+ raise SystemExit("Input JSON must be an array")
235
+
236
+ output_path.write_text(render_markdown(results), encoding="utf-8")
237
+ print(f"Wrote markdown report: {output_path}")
238
+ return 0
239
+
240
+
241
+ if __name__ == "__main__":
242
+ raise SystemExit(main())
@@ -0,0 +1,102 @@
1
+ # npm/TypeScript Dependency & Package Review
2
+
3
+ ## Summary
4
+
5
+ - Package / repo:
6
+ - Version / commit:
7
+ - Reviewer:
8
+ - Date:
9
+ - Mode: package | library | application | repo
10
+ - Network metadata used: yes | no
11
+ - Decision: PASS_WITH_CAUTION | REVIEW_BEFORE_USE | BLOCK_UNTIL_REVIEW | QUARANTINE
12
+
13
+ ## Artifact identity
14
+
15
+ - npm package:
16
+ - npm version:
17
+ - Tarball URL:
18
+ - Tarball SHA-256:
19
+ - npm `dist.integrity`:
20
+ - Git repo:
21
+ - Git commit SHA:
22
+ - Lockfile hash:
23
+
24
+ ## Automated scanner output
25
+
26
+ - Markdown report:
27
+ - JSON report:
28
+ - SARIF report:
29
+ - Strict exit code:
30
+ - Counts:
31
+ - CRITICAL:
32
+ - HIGH:
33
+ - MEDIUM:
34
+ - LOW:
35
+ - INFO:
36
+
37
+ ## Manual review checklist
38
+
39
+ ### package.json
40
+
41
+ - [ ] No unexpected install-phase lifecycle scripts.
42
+ - [ ] All lifecycle scripts have a documented legitimate purpose.
43
+ - [ ] No download+execute patterns in scripts.
44
+ - [ ] No Git/URL/File dependency in production or optional dependencies unless allowlisted.
45
+ - [ ] No suspicious `overrides` or `resolutions`.
46
+ - [ ] No unexpected `bundleDependencies`.
47
+ - [ ] CLI `bin` entrypoints are readable and reviewed.
48
+
49
+ ### Lockfile
50
+
51
+ - [ ] All registry tarballs have integrity.
52
+ - [ ] No unexpected Git/URL/File `resolved` entries.
53
+ - [ ] `hasInstallScript` entries are reviewed and allowlisted.
54
+ - [ ] Optional dependencies are reviewed or omitted.
55
+ - [ ] Dependency diff is understood.
56
+
57
+ ### Tarball contents
58
+
59
+ - [ ] Tarball matches expected repo/build output.
60
+ - [ ] No unexpected `setup.mjs`, `install.js`, `postinstall.js`, large one-line JS, hidden configs or native binaries.
61
+ - [ ] `.vscode`, `.claude`, `.cursor`, `.devcontainer`, `.npmrc` are absent unless explicitly expected.
62
+
63
+ ### Code behavior
64
+
65
+ - [ ] No network+exec chain.
66
+ - [ ] No credential access+network chain.
67
+ - [ ] No GitHub API write behavior except documented release tooling.
68
+ - [ ] No obfuscated payload or unexplained minified large file.
69
+ - [ ] No cloud metadata, Vault or local token harvesting.
70
+
71
+ ### CI/CD
72
+
73
+ - [ ] No unsafe `pull_request_target` or `workflow_run` path.
74
+ - [ ] Actions are pinned to full-length commit SHA or otherwise controlled by org policy.
75
+ - [ ] Token permissions are minimal.
76
+ - [ ] Publish workflows run only from protected refs.
77
+ - [ ] Dependency review installs use `--ignore-scripts`.
78
+
79
+ ### TypeScript quality
80
+
81
+ - [ ] `tsconfig` uses strict settings or exceptions are documented.
82
+ - [ ] Declarations/types are published for libraries.
83
+ - [ ] Tests, lint and typecheck exist.
84
+ - [ ] README, LICENSE, SECURITY.md, repository metadata and engines are present.
85
+
86
+ ## Findings
87
+
88
+ | Severity | File:line | Finding | Evidence | Recommendation | Status |
89
+ | -------- | --------- | ------- | -------- | -------------- | ------------------ |
90
+ | | | | | | needs-human-review |
91
+
92
+ ## Decision rationale
93
+
94
+ Explain why the package is accepted, blocked or quarantined.
95
+
96
+ ## Follow-up actions
97
+
98
+ - [ ] Upstream issue opened:
99
+ - [ ] Dependency pinned/reverted:
100
+ - [ ] Token rotation needed:
101
+ - [ ] Allowlist entry created:
102
+ - [ ] CI policy updated: