@topy-ai/maggie 0.7.55 → 0.7.57

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.
Files changed (73) hide show
  1. package/README-zh-TW.md +7 -3
  2. package/README.md +42 -8
  3. package/bin/maggie.js +4 -0
  4. package/bundled-contracts/maggie-deployment/readiness-v1.schema.json +2 -1
  5. package/bundled-contracts/maggie-security/finding-example-v1.json +89 -0
  6. package/bundled-contracts/maggie-security/finding-v1.schema.json +63 -0
  7. package/bundled-contracts/maggie-security/findings-set-example-v1.json +148 -0
  8. package/bundled-contracts/maggie-security/findings-set-v1.schema.json +32 -0
  9. package/bundled-contracts/maggie-security/protect-plan-v1.schema.json +28 -0
  10. package/bundled-contracts/maggie-security/report-v1.schema.json +29 -0
  11. package/bundled-contracts/maggie-security/stack-example-v1.json +66 -0
  12. package/bundled-contracts/maggie-security/stack-upload-v1.schema.json +22 -0
  13. package/bundled-contracts/maggie-security/stack-v1.schema.json +37 -0
  14. package/bundled-contracts/maggie-security/tools-lock-v1.schema.json +36 -0
  15. package/bundled-skills/catalog.json +4 -0
  16. package/bundled-skills/maggie-security/SKILL.md +173 -0
  17. package/bundled-skills/maggie-security/agents/openai.yaml +4 -0
  18. package/bundled-skills/maggie-security/data/LICENSE-NOTICE.md +24 -0
  19. package/bundled-skills/maggie-security/data/asvs-map.json +209 -0
  20. package/bundled-skills/maggie-security/data/fail2ban/filter.d/maggie-wordpress.conf +6 -0
  21. package/bundled-skills/maggie-security/data/semgrep/LICENSE-NOTICE.md +5 -0
  22. package/bundled-skills/maggie-security/data/semgrep/VERSION +1 -0
  23. package/bundled-skills/maggie-security/data/semgrep/rules/dockerfile.yaml +14 -0
  24. package/bundled-skills/maggie-security/data/semgrep/rules/generic.yaml +26 -0
  25. package/bundled-skills/maggie-security/data/semgrep/rules/js.yaml +216 -0
  26. package/bundled-skills/maggie-security/data/semgrep/rules/php-debug-ok.yaml +17 -0
  27. package/bundled-skills/maggie-security/data/semgrep/rules/php-debug.yaml +17 -0
  28. package/bundled-skills/maggie-security/data/semgrep/rules/php.yaml +101 -0
  29. package/bundled-skills/maggie-security/data/semgrep/tests/dockerfile.dockerfile +10 -0
  30. package/bundled-skills/maggie-security/data/semgrep/tests/generic.astro +11 -0
  31. package/bundled-skills/maggie-security/data/semgrep/tests/generic.conf +11 -0
  32. package/bundled-skills/maggie-security/data/semgrep/tests/js.jsx +95 -0
  33. package/bundled-skills/maggie-security/data/semgrep/tests/php-debug-ok.php +5 -0
  34. package/bundled-skills/maggie-security/data/semgrep/tests/php-debug.php +3 -0
  35. package/bundled-skills/maggie-security/data/semgrep/tests/php.php +55 -0
  36. package/bundled-skills/maggie-security/evals/evals.json +97 -0
  37. package/bundled-skills/maggie-security/references/commands.md +125 -0
  38. package/bundled-skills/maggie-security/references/cve-response.md +24 -0
  39. package/bundled-skills/maggie-security/references/explaining-findings.md +57 -0
  40. package/bundled-skills/maggie-security/references/findings.md +39 -0
  41. package/bundled-skills/maggie-security/references/ownership-and-consent.md +29 -0
  42. package/bundled-skills/maggie-security/references/site-types.md +22 -0
  43. package/bundled-skills/maggie-security/references/tools-and-licences.md +16 -0
  44. package/bundled-skills/maggie-security/references/workflows.md +23 -0
  45. package/bundled-skills/maggie-security/registry/security-tools.registry.json +654 -0
  46. package/bundled-skills/maggie-security/routines.example.json +27 -0
  47. package/bundled-tools/clis/maggie_deployment_readiness.py +17 -2
  48. package/bundled-tools/clis/maggie_security.py +342 -0
  49. package/bundled-tools/clis/maggie_security_agent.py +326 -0
  50. package/bundled-tools/clis/maggie_security_code.py +650 -0
  51. package/bundled-tools/clis/maggie_security_core.py +1512 -0
  52. package/bundled-tools/clis/maggie_security_deps.py +560 -0
  53. package/bundled-tools/clis/maggie_security_health.py +415 -0
  54. package/bundled-tools/clis/maggie_security_host.py +511 -0
  55. package/bundled-tools/clis/maggie_security_hosted.py +115 -0
  56. package/bundled-tools/clis/maggie_security_intel.py +1294 -0
  57. package/bundled-tools/clis/maggie_security_inventory.py +1023 -0
  58. package/bundled-tools/clis/maggie_security_logs.py +777 -0
  59. package/bundled-tools/clis/maggie_security_mcp.py +484 -0
  60. package/bundled-tools/clis/maggie_security_mirror.py +122 -0
  61. package/bundled-tools/clis/maggie_security_protect.py +519 -0
  62. package/bundled-tools/clis/maggie_security_report.py +506 -0
  63. package/bundled-tools/clis/maggie_security_standards.py +214 -0
  64. package/bundled-tools/clis/maggie_security_versions.py +381 -0
  65. package/bundled-tools/clis/maggie_security_web.py +1247 -0
  66. package/bundled-tools/clis/security_data/eol-snapshot.json +1 -0
  67. package/bundled-tools/clis/security_data/gitleaks-rules.json +3812 -0
  68. package/bundled-tools/clis/security_data/maltrail-lists.json +1 -0
  69. package/bundled-tools/clis/security_data/public-suffix-icann.txt +7374 -0
  70. package/bundled-tools/clis/security_data/retire-extractors.json +695 -0
  71. package/bundled-tools/clis/security_data/sigma-web.json +6514 -0
  72. package/bundled-tools/clis/security_data/tools.lock.json +358 -0
  73. package/package.json +1 -1
@@ -0,0 +1,326 @@
1
+ #!/usr/bin/env python3
2
+ """Agent security (docs/maggie-security/10-agent-security-prd.md):
3
+ ``skills-scan``, ``mcp-self-check``, ``github-alerts``.
4
+
5
+ - ``skills-scan`` runs NVIDIA SkillSpector (Apache-2.0, external, Python 3.12+)
6
+ with ``--no-llm`` forced and its LLM provider variable removed, and maps the
7
+ report to findings. Maggie re-applies ``--max-risk`` from the JSON instead
8
+ of trusting the upstream threshold. LLM mode is not offered here: routing
9
+ SkillSpector through a gateway cannot lock the provider (global BYOK rule).
10
+ - ``mcp-self-check`` reviews an MCP tool registry, an exported ``tools/list``
11
+ and the server config against the checks in PRD 10 section 8 (risk and
12
+ confirmations, annotations, hidden text and cross-tool instructions in
13
+ descriptions, untrusted results, clipping, transport auth, caps, audit,
14
+ description drift between releases).
15
+ - ``github-alerts`` turns the person's own GitHub Dependabot, code-scanning
16
+ and secret-scanning alerts into findings, from a file the agent exported
17
+ with github-mcp-server, or directly with ``GITHUB_TOKEN`` (read-only; the
18
+ token is never stored). A secret alert's value is never read.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import json
25
+ import os
26
+ import re
27
+ import tempfile
28
+ import unicodedata
29
+ from pathlib import Path
30
+
31
+ import maggie_security_core as core
32
+
33
+ core.SOURCES.setdefault("github-api", {"hosts": ["api.github.com"], "ttl": "0s", "url": "https://api.github.com/"})
34
+ SKILLSPECTOR_SEVERITY = {"CRITICAL": "critical", "HIGH": "high", "MEDIUM": "medium", "LOW": "low", "INFO": "info"}
35
+
36
+
37
+ # --------------------------------------------------------------------------- skills-scan
38
+
39
+
40
+ def skillspector_to_findings(ctx: core.Context, report: dict, target: str) -> list[dict]:
41
+ findings = []
42
+ # SkillSpector 2.x JSON: issues[] with id, category, pattern, severity, confidence, location{file, start_line}, finding, explanation, remediation.
43
+ for item in report.get("issues") or report.get("findings") or []:
44
+ confidence = float(item.get("confidence") or 0.5)
45
+ category = item.get("category") or "Security"
46
+ excerpt = core.clean_excerpt(item.get("finding") or item.get("code_snippet") or "", 200)
47
+ location = item.get("location") or {}
48
+ skill_file = location.get("file") or item.get("file") or "SKILL.md"
49
+ item = {**item, "rule_id": item.get("id") or item.get("rule_id"), "start_line": location.get("start_line") or item.get("start_line"),
50
+ "message": item.get("pattern") or item.get("message")}
51
+ findings.append(core.make_finding(
52
+ kind="agent-skill", command="skills-scan", primary_id=str(item.get("rule_id")), site=ctx.site,
53
+ title=f"{category}: {core.clean_excerpt(item.get('message') or item.get('rule_id'), 110)}",
54
+ severity=SKILLSPECTOR_SEVERITY.get(str(item.get("severity", "")).upper(), "low"), severity_source="tool",
55
+ confidence="confirmed" if confidence >= 0.8 else "likely" if confidence >= 0.5 else "possible",
56
+ asvs_ids=["V15.2.4"] if "supply" in category.lower() else [],
57
+ target={"path": f"{target.rstrip('/')}/{skill_file}" if not target.endswith(skill_file) else target, "url": item.get("source_url")},
58
+ evidence=[{"path": skill_file, "line": int(item.get("start_line") or 1), "excerpt": excerpt}],
59
+ tool="skillspector", rule_id=str(item.get("rule_id")),
60
+ remediation={"summary": core.clean_excerpt(item.get("remediation") or item.get("explanation") or "Review before installing.", 400),
61
+ "steps": [], "snippets": [], "references": ["https://github.com/NVIDIA/SkillSpector"], "cheat_sheet": None},
62
+ untrusted={"matched": excerpt, "explanation": core.clean_excerpt(item.get("explanation") or "", 300)}))
63
+ return findings
64
+
65
+
66
+ def skills_scan(ctx: core.Context):
67
+ args = ctx.args
68
+ target = args.target
69
+ if args.report_json:
70
+ report = core.read_json(Path(args.report_json))
71
+ else:
72
+ if not ("://" in target or Path(target).exists()):
73
+ raise core.UsageError(f"not found: {target}")
74
+ with tempfile.TemporaryDirectory() as tmp:
75
+ out = Path(tmp) / "report.json"
76
+ argv = ["scan", target, "--format", "json", "--output", str(out)]
77
+ if args.kind == "mcp-registry":
78
+ argv.append("--mcp-registry")
79
+ if args.recursive:
80
+ argv.append("--recursive")
81
+ if args.baseline:
82
+ argv += ["--baseline", args.baseline]
83
+ env = {k: "" for k in os.environ if k.startswith("SKILLSPECTOR_PROVIDER")}
84
+ if not args.allow_osv:
85
+ env["SKILLSPECTOR_OFFLINE"] = "1"
86
+ proc = core.run_tool("skillspector", argv, ctx.lock, timeout=1800, env=env)
87
+ if proc.returncode == 2:
88
+ raise core.UsageError(f"SkillSpector could not read the target: {core.clean_excerpt(proc.stderr.decode(errors='replace'), 300)}")
89
+ if not out.is_file():
90
+ raise core.DegradedError(f"SkillSpector produced no report: {core.clean_excerpt(proc.stderr.decode(errors='replace'), 300)}")
91
+ report = core.read_json(out)
92
+ ctx.run["tools"].append({"name": "skillspector", "version": ctx.lock["tools"]["skillspector"]["version"], "sha256": None})
93
+ findings = skillspector_to_findings(ctx, report, target)
94
+ assessment = report.get("risk_assessment") or {}
95
+ risk = assessment.get("score", report.get("risk_score"))
96
+ ctx.run["inputs"]["skillspector"] = {"risk_score": risk, "risk_severity": assessment.get("severity", report.get("risk_severity")),
97
+ "recommendation": assessment.get("recommendation", report.get("risk_recommendation")), "max_risk": args.max_risk,
98
+ "completeness": (report.get("analysis_completeness") or {}).get("status")}
99
+ doc = ctx.finish(findings, covered=["*"])
100
+ if risk is not None and float(risk) > args.max_risk:
101
+ doc["run"]["refused"].append(f"risk score {risk} is above the limit {args.max_risk}: do not install")
102
+ return doc
103
+
104
+
105
+ # --------------------------------------------------------------------------- mcp-self-check
106
+
107
+ HIDDEN = re.compile("[​-‏‪-‮⁠-⁤]")
108
+ CROSS_TOOL = re.compile(r"(?i)\b(ignore (all |any )?(previous|prior|above)|before (calling|using) any other tool|you must (always )?call|"
109
+ r"do not (tell|inform) the user|<\s*(system|tool|important)\s*>|send (the|all) .* to)\b")
110
+
111
+
112
+ def mcp_self_check(ctx: core.Context):
113
+ args = ctx.args
114
+ import maggie_security_mcp as mcp
115
+
116
+ registry_path = Path(args.registry) if args.registry else mcp.registry_path()
117
+ registry = core.read_json(registry_path)
118
+ tools = {t["name"]: t for t in registry.get("tools") or []}
119
+ listed = core.read_json(Path(args.mcp_tools)).get("tools") if args.mcp_tools else None
120
+ if listed is None and not args.registry:
121
+ listed = [mcp.Server(ctx.project, "all", None).describe(t) for t in mcp.Server(ctx.project, "all", None).tools.values()]
122
+ server_config = core.read_json(Path(args.server_config)) if args.server_config else {}
123
+ findings = []
124
+ where = str(registry_path.name)
125
+
126
+ def add(check: str, title: str, severity: str, tool: str | None = None, summary: str = "") -> None:
127
+ findings.append(core.make_finding(kind="agent-skill", command="mcp-self-check", primary_id=f"{check}:{tool or '-'}", site=ctx.site,
128
+ title=f"{check}: {title}", severity=severity, severity_source="maggie", confidence="confirmed",
129
+ target={"path": where}, evidence=[{"path": where, "excerpt": tool or ""}], tool="maggie", rule_id=check,
130
+ remediation={"summary": summary or title, "steps": [], "snippets": [],
131
+ "references": ["https://cheatsheetseries.owasp.org/cheatsheets/MCP_Security_Cheat_Sheet.html"],
132
+ "cheat_sheet": "MCP Security"}))
133
+
134
+ from maggie_assistant_tools import validate_registry
135
+
136
+ validation = validate_registry(registry)
137
+ for error in validation["errors"]:
138
+ add("MCP-01", f"registry: {error}", "high")
139
+ for name, tool in tools.items():
140
+ if tool.get("risk") not in ("read", "never") and not (tool.get("user") or {}).get("confirm"):
141
+ add("MCP-01", f"write tool {name} has no approval sentence (user.confirm)", "high", name)
142
+ text = tool.get("description", "") + " " + json.dumps(tool.get("user") or {})
143
+ if HIDDEN.search(text) or any(unicodedata.category(ch) == "Cf" for ch in text):
144
+ add("MCP-07", f"{name} has hidden or bidirectional control characters in its text", "high", name)
145
+ if CROSS_TOOL.search(tool.get("description", "")):
146
+ add("MCP-07", f"{name}'s description contains instructions aimed at the model or other tools", "high", name)
147
+ for url in re.findall(r"https?://([^/\s\"']+)", tool.get("description", "")):
148
+ if url not in (args.allow_host or []):
149
+ add("MCP-07", f"{name}'s description links to {url}", "medium", name, "Keep links out of model-facing descriptions, or allowlist the host.")
150
+ if re.search(r"(?i)(advisor|log|page|feed|third-party|skill)", tool.get("description", "")) and tool.get("risk") == "read" \
151
+ and "untrusted" not in tool.get("description", "").lower() and not tool.get("untrusted") and name in ("security_findings_list", "security_finding_get"):
152
+ add("MCP-09", f"{name} returns third-party text without saying it is untrusted", "high", name)
153
+ if listed is not None:
154
+ listed_names = {t["name"] for t in listed}
155
+ for name, tool in tools.items():
156
+ if tool.get("risk") == "never" and name in listed_names:
157
+ add("MCP-03", f"'never' tool {name} is exposed over MCP", "critical", name)
158
+ for entry in listed:
159
+ tool = tools.get(entry["name"])
160
+ if not tool:
161
+ add("MCP-08", f"{entry['name']} is served but not in the registry", "high", entry["name"])
162
+ continue
163
+ hints = entry.get("annotations") or {}
164
+ if tool.get("risk") == "read" and hints.get("readOnlyHint") is not True:
165
+ add("MCP-02", f"{entry['name']} is a read tool without readOnlyHint", "high", entry["name"])
166
+ if tool.get("risk") != "read" and hints.get("readOnlyHint"):
167
+ add("MCP-02", f"{entry['name']} writes but claims readOnlyHint", "high", entry["name"])
168
+ if tool.get("risk") in ("write-public", "never") and not hints.get("destructiveHint"):
169
+ add("MCP-02", f"{entry['name']} is a public/irreversible write without destructiveHint", "high", entry["name"])
170
+ schema = entry.get("inputSchema") or {}
171
+ if schema.get("additionalProperties") is not False:
172
+ add("MCP-05", f"{entry['name']} accepts arbitrary extra arguments (additionalProperties is not false)", "medium", entry["name"])
173
+ if tool.get("risk") != "read" and "confirm" not in (schema.get("properties") or {}):
174
+ add("MCP-06", f"write tool {entry['name']} has no preview → confirm step", "high", entry["name"])
175
+ if args.previous:
176
+ previous = {t["name"]: t for t in core.read_json(Path(args.previous)).get("tools") or []}
177
+ for entry in listed or []:
178
+ before = previous.get(entry["name"])
179
+ if before and (before.get("description") != entry.get("description") or before.get("inputSchema") != entry.get("inputSchema")):
180
+ add("MCP-08", f"{entry['name']} changed its description or input schema since the previous release: review it", "medium", entry["name"])
181
+ for server in registry.get("mcpServers") or []:
182
+ if server.get("auth") == "none":
183
+ writes = [n for n, t in tools.items() if server.get("name") in (t.get("mcpServers") or []) and t.get("risk") != "read"]
184
+ if writes:
185
+ add("MCP-04", f"public server {server.get('name')} exposes write tools: {', '.join(writes)}", "critical")
186
+ transport = server_config.get("transport")
187
+ if transport == "http":
188
+ if not server_config.get("auth"):
189
+ bind = str(server_config.get("bind", "0.0.0.0"))
190
+ if not bind.startswith(("127.", "localhost", "::1")):
191
+ add("MCP-11", f"HTTP transport without auth listens on {bind}", "critical")
192
+ if server_config.get("token_in_query"):
193
+ add("MCP-11", "access tokens are accepted in the URL query string", "critical")
194
+ writes = [n for n, t in tools.items() if t.get("risk") not in ("read", "never")]
195
+ unattended = registry.get("unattended") or {}
196
+ if writes and not unattended and any("routine" in (t.get("description") or "").lower() for t in tools.values()):
197
+ add("MCP-12", "tools meant for scheduled agents have no server-side daily caps (unattended)", "medium")
198
+ if writes and not any((t.get("audit") or {}) for t in tools.values() if t.get("risk") not in ("read", "never")) and args.require_audit:
199
+ add("MCP-13", "write tools declare no audit settings", "medium")
200
+ evals = core.ROOT / "skills" / "maggie-security" / "evals" / "evals.json"
201
+ if args.registry is None and evals.is_file() and "security_ledger_mark" not in evals.read_text():
202
+ add("MCP-15", "no prompt-injection case in the tool-selection evals", "low")
203
+ ctx.run["inputs"].update({"registry": str(registry_path), "tools": len(tools), "listed": len(listed or []), "checks": "MCP-01..MCP-15"})
204
+ return ctx.finish(findings, covered=["*"])
205
+
206
+
207
+ # --------------------------------------------------------------------------- github-alerts
208
+
209
+
210
+ def github_alert_to_finding(ctx: core.Context, alert: dict, repo: str) -> dict | None:
211
+ state = alert.get("state")
212
+ url = alert.get("html_url")
213
+ if "security_advisory" in alert or "dependency" in alert:
214
+ advisory = alert.get("security_advisory") or {}
215
+ vuln = alert.get("security_vulnerability") or {}
216
+ package = (alert.get("dependency") or {}).get("package") or vuln.get("package") or {}
217
+ first = (vuln.get("first_patched_version") or {}).get("identifier")
218
+ cvss = advisory.get("cvss") or {}
219
+ aliases = [i.get("value") for i in advisory.get("identifiers") or [] if i.get("value")] or [advisory.get("ghsa_id")]
220
+ return core.make_finding(
221
+ kind="vulnerability", command="github-alerts", primary_id=advisory.get("cve_id") or advisory.get("ghsa_id") or str(alert.get("number")), site=ctx.site,
222
+ title=f"{package.get('name')}: {core.clean_excerpt(advisory.get('summary') or advisory.get('ghsa_id') or 'Dependabot alert', 150)}",
223
+ severity={"critical": "critical", "high": "high", "medium": "medium", "moderate": "medium", "low": "low"}.get(str(advisory.get("severity")).lower(), "medium"),
224
+ severity_source="vendor", cvss={"version": "3.1", "score": float(cvss["score"]), "vector": cvss.get("vector_string") or ""} if cvss.get("score") else None,
225
+ confidence="likely", aliases=[a for a in aliases if a],
226
+ affected={"purl": core.make_purl(package.get("ecosystem", ""), package.get("name", "")), "ecosystem": package.get("ecosystem"),
227
+ "name": package.get("name") or "", "version": None, "component_type": "library", "direct": None, "scope": "runtime"},
228
+ fix={"fixed_versions": [first] if first else [], "recommended": first, "available": bool(first)},
229
+ target={"path": (alert.get("dependency") or {}).get("manifest_path"), "url": url}, feed="github", rule_id=advisory.get("ghsa_id"),
230
+ remediation={"summary": f"Update {package.get('name')} to {first} or later." if first else "No patched version yet.", "steps": [], "snippets": [],
231
+ "references": [url] if url else [], "cheat_sheet": None}), state
232
+ if "rule" in alert:
233
+ rule = alert.get("rule") or {}
234
+ instance = alert.get("most_recent_instance") or {}
235
+ location = instance.get("location") or {}
236
+ return core.make_finding(
237
+ kind="code", command="github-alerts", primary_id=str(rule.get("id")), site=ctx.site,
238
+ title=f"{rule.get('description') or rule.get('id')} ({location.get('path')})"[:300],
239
+ severity={"critical": "critical", "high": "high", "medium": "medium", "low": "low"}.get(str(rule.get("security_severity_level")).lower(), "medium"),
240
+ severity_source="tool", confidence="likely", target={"path": location.get("path"), "url": url},
241
+ evidence=[{"path": location.get("path") or "", "line": location.get("start_line") or 1}], feed="github", rule_id=str(rule.get("id")),
242
+ tool=(alert.get("tool") or {}).get("name"),
243
+ remediation={"summary": "See the code scanning alert.", "steps": [], "snippets": [], "references": [url] if url else [], "cheat_sheet": None}), state
244
+ if "secret_type" in alert:
245
+ return core.make_finding(
246
+ kind="secret", command="github-alerts", primary_id=f"gh-secret-{alert.get('number')}", site=ctx.site,
247
+ title=f"GitHub found a {alert.get('secret_type_display_name') or alert.get('secret_type')} in {repo}", severity="high", severity_source="vendor",
248
+ confidence="confirmed", target={"url": url, "path": repo}, feed="github", rule_id=str(alert.get("secret_type")),
249
+ remediation={"summary": "Revoke the secret with its provider first, then close the alert.", "steps": [], "snippets": [],
250
+ "references": [url] if url else [], "cheat_sheet": None}), state
251
+ return None
252
+
253
+
254
+ def github_alerts(ctx: core.Context):
255
+ args = ctx.args
256
+ alerts: list[tuple[str, dict]] = []
257
+ if args.from_file:
258
+ data = core.read_json(Path(args.from_file))
259
+ items = data if isinstance(data, list) else data.get("alerts") or data.get("items") or []
260
+ alerts = [(args.repo[0] if args.repo else "repo", a) for a in items]
261
+ else:
262
+ token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
263
+ if not token:
264
+ raise core.PolicyError("direct mode needs your own GITHUB_TOKEN in the environment (read: Dependabot alerts, security_events); "
265
+ "or export alerts with github-mcp-server and pass --from-file")
266
+ if not args.repo:
267
+ raise core.UsageError("give --repo OWNER/NAME")
268
+ for repo in args.repo:
269
+ for kind in (args.include or "dependabot,code-scanning,secret-scanning").split(","):
270
+ url = f"https://api.github.com/repos/{repo}/{kind}/alerts?state={args.state}&per_page=100"
271
+ ctx.guard.check("github-api", url)
272
+ try:
273
+ status, _h, body = core.http_request(url, headers={"Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json",
274
+ "X-GitHub-Api-Version": "2022-11-28"}, timeout=30, retries=1)
275
+ except core.DegradedError as error:
276
+ ctx.degrade(f"github-{kind}", str(error))
277
+ continue
278
+ if status in (403, 404):
279
+ ctx.degrade(f"github-{kind}", f"HTTP {status}: not available on this plan or token scope (not the same as clean)")
280
+ continue
281
+ alerts += [(repo, a) for a in json.loads(body.decode() or "[]")]
282
+ findings = []
283
+ for repo, alert in alerts:
284
+ converted = github_alert_to_finding(ctx, alert, repo)
285
+ if not converted:
286
+ continue
287
+ finding, state = converted
288
+ if state not in (None, "open"):
289
+ continue
290
+ findings.append(finding)
291
+ ctx.run["inputs"].update({"repos": args.repo, "alerts": len(alerts), "mode": "file" if args.from_file else "direct"})
292
+ return ctx.finish(findings, covered=["*"] if not ctx.run["degraded"] else [])
293
+
294
+
295
+ def _skills(p: argparse.ArgumentParser) -> None:
296
+ p.add_argument("target", help="skill folder, SKILL.md, .zip, git URL, or MCP registry JSON/URL")
297
+ p.add_argument("--kind", choices=["skill", "mcp-registry"], default="skill")
298
+ p.add_argument("--max-risk", type=float, default=50.0)
299
+ p.add_argument("--recursive", action="store_true")
300
+ p.add_argument("--baseline")
301
+ p.add_argument("--allow-osv", action="store_true", help="let SkillSpector look up dependency CVEs on osv.dev")
302
+ p.add_argument("--report-json", help="parse a saved SkillSpector JSON report")
303
+
304
+
305
+ def _mcp(p: argparse.ArgumentParser) -> None:
306
+ p.add_argument("--registry", help="maggie-assistant-tool-registry.v1 file (default: Maggie Security's own)")
307
+ p.add_argument("--mcp-tools", help="exported tools/list JSON from the running server")
308
+ p.add_argument("--server-config", help="transport/auth settings JSON")
309
+ p.add_argument("--previous", help="the previous release's tools/list export (description drift)")
310
+ p.add_argument("--allow-host", action="append", help="host allowed in descriptions")
311
+ p.add_argument("--require-audit", action="store_true")
312
+
313
+
314
+ def _github(p: argparse.ArgumentParser) -> None:
315
+ p.add_argument("--repo", action="append")
316
+ p.add_argument("--state", default="open")
317
+ p.add_argument("--include", help="dependabot,code-scanning,secret-scanning")
318
+ p.add_argument("--from-file", help="alerts exported by github-mcp-server (list_*_alerts)")
319
+
320
+
321
+ COMMANDS = [
322
+ core.Command("skills-scan", "check an agent skill or MCP server before installing it (SkillSpector, no LLM)", ["osv-api"], _skills, skills_scan),
323
+ core.Command("mcp-self-check", "review an MCP tool registry and server settings against the OWASP MCP checks", [], _mcp, mcp_self_check),
324
+ core.Command("github-alerts", "your GitHub Dependabot, code-scanning and secret-scanning alerts as findings (your own token or an export)",
325
+ ["github-api"], _github, github_alerts),
326
+ ]