@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.
- package/README-zh-TW.md +7 -3
- package/README.md +42 -8
- package/bin/maggie.js +4 -0
- package/bundled-contracts/maggie-deployment/readiness-v1.schema.json +2 -1
- package/bundled-contracts/maggie-security/finding-example-v1.json +89 -0
- package/bundled-contracts/maggie-security/finding-v1.schema.json +63 -0
- package/bundled-contracts/maggie-security/findings-set-example-v1.json +148 -0
- package/bundled-contracts/maggie-security/findings-set-v1.schema.json +32 -0
- package/bundled-contracts/maggie-security/protect-plan-v1.schema.json +28 -0
- package/bundled-contracts/maggie-security/report-v1.schema.json +29 -0
- package/bundled-contracts/maggie-security/stack-example-v1.json +66 -0
- package/bundled-contracts/maggie-security/stack-upload-v1.schema.json +22 -0
- package/bundled-contracts/maggie-security/stack-v1.schema.json +37 -0
- package/bundled-contracts/maggie-security/tools-lock-v1.schema.json +36 -0
- package/bundled-skills/catalog.json +4 -0
- package/bundled-skills/maggie-security/SKILL.md +173 -0
- package/bundled-skills/maggie-security/agents/openai.yaml +4 -0
- package/bundled-skills/maggie-security/data/LICENSE-NOTICE.md +24 -0
- package/bundled-skills/maggie-security/data/asvs-map.json +209 -0
- package/bundled-skills/maggie-security/data/fail2ban/filter.d/maggie-wordpress.conf +6 -0
- package/bundled-skills/maggie-security/data/semgrep/LICENSE-NOTICE.md +5 -0
- package/bundled-skills/maggie-security/data/semgrep/VERSION +1 -0
- package/bundled-skills/maggie-security/data/semgrep/rules/dockerfile.yaml +14 -0
- package/bundled-skills/maggie-security/data/semgrep/rules/generic.yaml +26 -0
- package/bundled-skills/maggie-security/data/semgrep/rules/js.yaml +216 -0
- package/bundled-skills/maggie-security/data/semgrep/rules/php-debug-ok.yaml +17 -0
- package/bundled-skills/maggie-security/data/semgrep/rules/php-debug.yaml +17 -0
- package/bundled-skills/maggie-security/data/semgrep/rules/php.yaml +101 -0
- package/bundled-skills/maggie-security/data/semgrep/tests/dockerfile.dockerfile +10 -0
- package/bundled-skills/maggie-security/data/semgrep/tests/generic.astro +11 -0
- package/bundled-skills/maggie-security/data/semgrep/tests/generic.conf +11 -0
- package/bundled-skills/maggie-security/data/semgrep/tests/js.jsx +95 -0
- package/bundled-skills/maggie-security/data/semgrep/tests/php-debug-ok.php +5 -0
- package/bundled-skills/maggie-security/data/semgrep/tests/php-debug.php +3 -0
- package/bundled-skills/maggie-security/data/semgrep/tests/php.php +55 -0
- package/bundled-skills/maggie-security/evals/evals.json +97 -0
- package/bundled-skills/maggie-security/references/commands.md +125 -0
- package/bundled-skills/maggie-security/references/cve-response.md +24 -0
- package/bundled-skills/maggie-security/references/explaining-findings.md +57 -0
- package/bundled-skills/maggie-security/references/findings.md +39 -0
- package/bundled-skills/maggie-security/references/ownership-and-consent.md +29 -0
- package/bundled-skills/maggie-security/references/site-types.md +22 -0
- package/bundled-skills/maggie-security/references/tools-and-licences.md +16 -0
- package/bundled-skills/maggie-security/references/workflows.md +23 -0
- package/bundled-skills/maggie-security/registry/security-tools.registry.json +654 -0
- package/bundled-skills/maggie-security/routines.example.json +27 -0
- package/bundled-tools/clis/maggie_deployment_readiness.py +17 -2
- package/bundled-tools/clis/maggie_security.py +342 -0
- package/bundled-tools/clis/maggie_security_agent.py +326 -0
- package/bundled-tools/clis/maggie_security_code.py +650 -0
- package/bundled-tools/clis/maggie_security_core.py +1512 -0
- package/bundled-tools/clis/maggie_security_deps.py +560 -0
- package/bundled-tools/clis/maggie_security_health.py +415 -0
- package/bundled-tools/clis/maggie_security_host.py +511 -0
- package/bundled-tools/clis/maggie_security_hosted.py +115 -0
- package/bundled-tools/clis/maggie_security_intel.py +1294 -0
- package/bundled-tools/clis/maggie_security_inventory.py +1023 -0
- package/bundled-tools/clis/maggie_security_logs.py +777 -0
- package/bundled-tools/clis/maggie_security_mcp.py +484 -0
- package/bundled-tools/clis/maggie_security_mirror.py +122 -0
- package/bundled-tools/clis/maggie_security_protect.py +519 -0
- package/bundled-tools/clis/maggie_security_report.py +506 -0
- package/bundled-tools/clis/maggie_security_standards.py +214 -0
- package/bundled-tools/clis/maggie_security_versions.py +381 -0
- package/bundled-tools/clis/maggie_security_web.py +1247 -0
- package/bundled-tools/clis/security_data/eol-snapshot.json +1 -0
- package/bundled-tools/clis/security_data/gitleaks-rules.json +3812 -0
- package/bundled-tools/clis/security_data/maltrail-lists.json +1 -0
- package/bundled-tools/clis/security_data/public-suffix-icann.txt +7374 -0
- package/bundled-tools/clis/security_data/retire-extractors.json +695 -0
- package/bundled-tools/clis/security_data/sigma-web.json +6514 -0
- package/bundled-tools/clis/security_data/tools.lock.json +358 -0
- 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
|
+
]
|