@topy-ai/maggie 0.7.54 → 0.7.56
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 +25 -23
- package/README.md +93 -71
- package/bin/maggie.js +14 -2
- package/bundled-contracts/maggie-assistant-tools/booking-receptionist-example-v1.json +18 -2
- package/bundled-contracts/maggie-assistant-tools/report-v1.schema.json +43 -0
- package/bundled-contracts/maggie-assistant-tools/tool-registry-example-v1.json +249 -11
- package/bundled-contracts/maggie-assistant-tools/tool-registry-v1.schema.json +175 -0
- package/bundled-contracts/maggie-deployment/readiness-v1.schema.json +2 -1
- package/bundled-contracts/maggie-ops/agent-routines-v1.schema.json +48 -0
- 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 +5 -1
- package/bundled-skills/maggie-assistant-tools/SKILL.md +34 -1
- package/bundled-skills/maggie-assistant-tools/references/report-inbox.md +45 -0
- package/bundled-skills/maggie-assistant-tools/references/unattended-agents.md +70 -0
- package/bundled-skills/maggie-blog/SKILL.md +9 -2
- package/bundled-skills/maggie-booking/SKILL.md +1 -1
- package/bundled-skills/maggie-dash/SKILL.md +9 -0
- package/bundled-skills/maggie-deployment/SKILL.md +77 -1
- package/bundled-skills/maggie-deployment/references/vps.md +5 -0
- package/bundled-skills/maggie-marketplace/SKILL.md +21 -1
- package/bundled-skills/maggie-ops/SKILL.md +28 -0
- package/bundled-skills/maggie-ops/references/agents-repository.md +108 -0
- package/bundled-skills/maggie-ops/references/ops-autopilot.md +65 -0
- package/bundled-skills/maggie-ops/references/routine-mode.md +92 -0
- package/bundled-skills/maggie-security/SKILL.md +172 -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 +111 -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 +628 -0
- package/bundled-skills/maggie-security/routines.example.json +27 -0
- package/bundled-skills/maggie-seo-geo/SKILL.md +9 -1
- package/bundled-skills/maggie-social-share/SKILL.md +20 -0
- package/bundled-tools/clis/maggie.py +63 -3
- package/bundled-tools/clis/maggie_assistant_tools.py +225 -1
- package/bundled-tools/clis/maggie_deployment_readiness.py +17 -2
- package/bundled-tools/clis/maggie_host_runtime.py +458 -0
- package/bundled-tools/clis/maggie_live_urls.py +11 -2
- package/bundled-tools/clis/maggie_ops.py +2 -0
- package/bundled-tools/clis/maggie_ops_routines.py +352 -0
- package/bundled-tools/clis/maggie_query_ideas.py +154 -0
- 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 +473 -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 +936 -0
- package/bundled-tools/clis/maggie_skill_catalog.py +16 -2
- package/bundled-tools/clis/maggie_social_share.py +101 -1
- package/bundled-tools/clis/maggie_version_sources.py +93 -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/bundled-tools/integrations/maggie-api-pull.md +28 -1
- package/package.json +1 -1
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""``maggie security <command>``: one entry point for every security check.
|
|
3
|
+
|
|
4
|
+
Routes to the area modules (docs/maggie-security/00-foundation-prd.md section
|
|
5
|
+
6.1) and owns the housekeeping commands:
|
|
6
|
+
|
|
7
|
+
- ``doctor``: what applies to this project, which tools and feeds are ready,
|
|
8
|
+
ownership, consents, ledger size and last runs. ``--fix`` only writes
|
|
9
|
+
``.maggie/security/config.json`` and a ``.gitignore`` entry.
|
|
10
|
+
- ``tools list|install|verify``: the pinned external scanners.
|
|
11
|
+
- ``feeds status|sync|verify``: the local feed cache.
|
|
12
|
+
- ``ledger list|show|accept|false-positive|reopen|compact``: finding status
|
|
13
|
+
history; the marks are local file writes only.
|
|
14
|
+
|
|
15
|
+
Exit codes: 0 ok, 1 findings at/above --fail-on, 2 usage, 3 degraded,
|
|
16
|
+
4 refused by policy, 5 tool missing or checksum mismatch.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import importlib
|
|
23
|
+
import json
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
28
|
+
|
|
29
|
+
import maggie_security_core as core # noqa: E402
|
|
30
|
+
|
|
31
|
+
AREA_MODULES = [
|
|
32
|
+
"maggie_security_intel", "maggie_security_deps", "maggie_security_code", "maggie_security_web",
|
|
33
|
+
"maggie_security_protect", "maggie_security_logs", "maggie_security_host", "maggie_security_health",
|
|
34
|
+
"maggie_security_standards", "maggie_security_agent", "maggie_security_report", "maggie_security_mcp", "maggie_security_hosted",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
# Commands from the PRDs that are not built yet, with their milestone, so the
|
|
38
|
+
# help screen and the skill can say "planned" instead of "unknown".
|
|
39
|
+
PLANNED: dict[str, str] = {}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def area_commands() -> dict[str, core.Command]:
|
|
43
|
+
commands: dict[str, core.Command] = {}
|
|
44
|
+
for name in AREA_MODULES:
|
|
45
|
+
try:
|
|
46
|
+
module = importlib.import_module(name)
|
|
47
|
+
except ModuleNotFoundError as error:
|
|
48
|
+
if error.name == name:
|
|
49
|
+
continue
|
|
50
|
+
raise
|
|
51
|
+
for command in getattr(module, "COMMANDS", []):
|
|
52
|
+
commands[command.name] = command
|
|
53
|
+
return commands
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# --------------------------------------------------------------------------- doctor
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def doctor(ctx: core.Context) -> dict:
|
|
60
|
+
project, state = ctx.project, ctx.state
|
|
61
|
+
config = state.config()
|
|
62
|
+
site_type = config.get("site_type") or core.detect_site_type(project)
|
|
63
|
+
domains = config.get("domains") or []
|
|
64
|
+
lock = core.load_lock(state)
|
|
65
|
+
tools = []
|
|
66
|
+
for name, spec in sorted((lock.get("tools") or {}).items()):
|
|
67
|
+
if spec.get("class") == "reference-only":
|
|
68
|
+
continue
|
|
69
|
+
_spec, path, how = core.resolve_tool(name, lock)
|
|
70
|
+
tools.append({"name": name, "version": spec["version"], "class": spec["class"], "how": how})
|
|
71
|
+
ownership = []
|
|
72
|
+
for domain in domains:
|
|
73
|
+
record_path = state.path("ownership", f"{domain}.json")
|
|
74
|
+
record = core.read_json(record_path) if record_path.is_file() else None
|
|
75
|
+
status = "unverified"
|
|
76
|
+
if record and record.get("verified_at"):
|
|
77
|
+
age = core.utcnow() - core.parse_time(record["verified_at"])
|
|
78
|
+
status = "verified" if age.days <= 90 else "expired"
|
|
79
|
+
ownership.append({"domain": domain, "status": status, "verified_at": (record or {}).get("verified_at")})
|
|
80
|
+
consent_path = state.path("consent.json")
|
|
81
|
+
consents = core.read_json(consent_path).get("grants", []) if consent_path.is_file() else []
|
|
82
|
+
ledger = core.Ledger(state.path("ledger.jsonl"))
|
|
83
|
+
history = ledger.history()
|
|
84
|
+
statuses: dict[str, int] = {}
|
|
85
|
+
for events in history.values():
|
|
86
|
+
status = core.Ledger.derive(events)["status"] or "unknown"
|
|
87
|
+
statuses[status] = statuses.get(status, 0) + 1
|
|
88
|
+
last_runs: dict[str, str] = {}
|
|
89
|
+
runs_dir = state.path("runs")
|
|
90
|
+
if runs_dir.is_dir():
|
|
91
|
+
for path in sorted(runs_dir.glob("*.json")):
|
|
92
|
+
try:
|
|
93
|
+
run = json.loads(path.read_text(encoding="utf-8"))["run"]
|
|
94
|
+
except (ValueError, KeyError, OSError):
|
|
95
|
+
continue
|
|
96
|
+
last_runs[run["command"]] = run["finished_at"]
|
|
97
|
+
available = sorted(area_commands())
|
|
98
|
+
applicable = [c for c in core.APPLICABLE.get(site_type, core.APPLICABLE["unknown"])]
|
|
99
|
+
gitignore = project / ".gitignore"
|
|
100
|
+
ignored = gitignore.is_file() and any(line.strip().rstrip("/*") in (".maggie/security", "/.maggie/security", ".maggie")
|
|
101
|
+
for line in gitignore.read_text(encoding="utf-8").splitlines())
|
|
102
|
+
problems = []
|
|
103
|
+
if not ignored and state.root.exists():
|
|
104
|
+
problems.append("`.maggie/security/` is not in .gitignore (run: maggie security doctor --fix)")
|
|
105
|
+
if not domains:
|
|
106
|
+
problems.append("no domains configured; web checks are skipped (set domains in .maggie/security/config.json)")
|
|
107
|
+
report = {
|
|
108
|
+
"schema": "maggie-security-status.v1",
|
|
109
|
+
"maggie_version": core.maggie_version(),
|
|
110
|
+
"project": str(project),
|
|
111
|
+
"site": ctx.site,
|
|
112
|
+
"site_type": site_type,
|
|
113
|
+
"domains": domains,
|
|
114
|
+
"commands": {"applicable": applicable, "available": available,
|
|
115
|
+
"applicable_not_built": [c for c in applicable if c not in available and c not in ("report",)]},
|
|
116
|
+
"tools": tools,
|
|
117
|
+
"feeds": core.FeedCache(state, ctx.guard).status(),
|
|
118
|
+
"ownership": ownership,
|
|
119
|
+
"consents": consents,
|
|
120
|
+
"ledger": {"ids": len(history), "by_status": statuses},
|
|
121
|
+
"last_runs": last_runs,
|
|
122
|
+
"gitignored": ignored,
|
|
123
|
+
"mirror": core.mirror_base(),
|
|
124
|
+
"problems": problems,
|
|
125
|
+
}
|
|
126
|
+
if getattr(ctx.args, "fix", False):
|
|
127
|
+
report["fixed"] = doctor_fix(project, state, site_type, ctx.site)
|
|
128
|
+
return report
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def doctor_fix(project: Path, state: core.State, site_type: str, site: str) -> list[str]:
|
|
132
|
+
done = []
|
|
133
|
+
state.ensure()
|
|
134
|
+
config_path = state.path("config.json")
|
|
135
|
+
if not config_path.is_file():
|
|
136
|
+
core.write_json(config_path, {"schema": "maggie-security-config.v1", "site": site, "site_type": site_type, "domains": [],
|
|
137
|
+
"schedules": {}, "thresholds": {"fail_on": "high"}, "opt_ins": {}})
|
|
138
|
+
done.append("created .maggie/security/config.json")
|
|
139
|
+
gitignore = project / ".gitignore"
|
|
140
|
+
lines = gitignore.read_text(encoding="utf-8").splitlines() if gitignore.is_file() else []
|
|
141
|
+
wanted = [".maggie/security/*", "!.maggie/security/config.json"]
|
|
142
|
+
missing = [line for line in wanted if line not in lines]
|
|
143
|
+
if missing:
|
|
144
|
+
with gitignore.open("a", encoding="utf-8") as handle:
|
|
145
|
+
if lines and lines[-1].strip():
|
|
146
|
+
handle.write("\n")
|
|
147
|
+
handle.write("# maggie security state (host evidence, findings); config.json is safe to commit\n")
|
|
148
|
+
handle.write("\n".join(missing) + "\n")
|
|
149
|
+
done.append("added .maggie/security/ to .gitignore")
|
|
150
|
+
ledger = core.Ledger(state.path("ledger.jsonl"))
|
|
151
|
+
if ledger.path.is_file():
|
|
152
|
+
result = ledger.compact()
|
|
153
|
+
done.append(f"compacted ledger {result['before']} -> {result['after']} events")
|
|
154
|
+
return done
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def doctor_text(report: dict) -> str:
|
|
158
|
+
lines = [f"maggie security doctor: {report['site']} ({report['site_type']})"]
|
|
159
|
+
lines.append(f" domains: {', '.join(report['domains']) or 'none'}")
|
|
160
|
+
lines.append(f" applicable: {', '.join(report['commands']['applicable'])}")
|
|
161
|
+
if report["commands"]["applicable_not_built"]:
|
|
162
|
+
lines.append(f" not built yet: {', '.join(report['commands']['applicable_not_built'])}")
|
|
163
|
+
for tool in report["tools"]:
|
|
164
|
+
lines.append(f" tool {tool['name']:<12} {tool['version']:<10} {tool['how']}")
|
|
165
|
+
for feed in report["feeds"]:
|
|
166
|
+
if feed["entries"]:
|
|
167
|
+
lines.append(f" feed {feed['id']:<16} {feed['fetched_at']}{' STALE' if feed['stale'] else ''}")
|
|
168
|
+
lines.append(f" ledger: {report['ledger']['ids']} findings {report['ledger']['by_status']}")
|
|
169
|
+
for problem in report["problems"]:
|
|
170
|
+
lines.append(f" ! {problem}")
|
|
171
|
+
for item in report.get("fixed", []):
|
|
172
|
+
lines.append(f" fixed: {item}")
|
|
173
|
+
return "\n".join(lines)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# --------------------------------------------------------------------------- tools, feeds, ledger
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def tools_cmd(ctx: core.Context) -> dict:
|
|
180
|
+
args = ctx.args
|
|
181
|
+
lock = core.load_lock(ctx.state)
|
|
182
|
+
names = [args.tool] if args.tool else sorted(lock.get("tools", {}))
|
|
183
|
+
if args.action == "list":
|
|
184
|
+
return {"schema": "maggie-security-tools.v1", "platform": core.platform_key(), "cache": str(core.tools_cache_dir()),
|
|
185
|
+
"tools": [{"name": n, **{k: v for k, v in lock["tools"][n].items() if k in ("version", "licence", "class", "notes")},
|
|
186
|
+
"how": core.resolve_tool(n, lock)[2]} for n in names]}
|
|
187
|
+
if args.action == "verify":
|
|
188
|
+
results = [core.verify_tool(n, lock) for n in names if lock["tools"][n]["class"] != "reference-only"]
|
|
189
|
+
bad = [r for r in results if not r["ok"] and r["how"] != "missing"]
|
|
190
|
+
if bad:
|
|
191
|
+
raise core.ToolError("; ".join(f"{r['tool']}: {r['problem']}" for r in bad))
|
|
192
|
+
return {"schema": "maggie-security-tools.v1", "results": results}
|
|
193
|
+
if not args.tool:
|
|
194
|
+
raise core.UsageError("tools install needs --tool NAME")
|
|
195
|
+
result = core.install_tool(args.tool, lock, ctx.guard)
|
|
196
|
+
ctx.state.ensure()
|
|
197
|
+
core.write_json(ctx.state.path("tools.lock.json"), core.read_json(core.packaged_lock_path()))
|
|
198
|
+
return result
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def feeds_cmd(ctx: core.Context) -> dict:
|
|
202
|
+
args = ctx.args
|
|
203
|
+
if args.action == "status":
|
|
204
|
+
return {"schema": "maggie-security-feeds.v1", "mirror": core.mirror_base(), "feeds": ctx.cache.status()}
|
|
205
|
+
if args.action == "mirror":
|
|
206
|
+
import maggie_security_mirror as mirror
|
|
207
|
+
|
|
208
|
+
if not args.dir:
|
|
209
|
+
raise core.UsageError("feeds mirror needs --dir DIR")
|
|
210
|
+
ecosystems = [e for e in (args.ecosystems or ",".join(mirror.DEFAULT_ECOSYSTEMS)).split(",") if e]
|
|
211
|
+
return mirror.build(ctx, Path(args.dir), ecosystems, args.days, args.to_project)
|
|
212
|
+
if args.action == "verify":
|
|
213
|
+
problems = ctx.cache.verify()
|
|
214
|
+
if problems:
|
|
215
|
+
raise core.DegradedError(json.dumps(problems))
|
|
216
|
+
return {"schema": "maggie-security-feeds.v1", "problems": []}
|
|
217
|
+
# sync: fetch fixed-URL sources (the ones that take parameters are fetched by their commands)
|
|
218
|
+
wanted = [args.source] if args.source else ["cvelistv5-delta", "kev", "node-core", "nuclei-cves"]
|
|
219
|
+
results = []
|
|
220
|
+
for source in wanted:
|
|
221
|
+
spec = core.SOURCES.get(source)
|
|
222
|
+
if not spec or "{" in spec["url"] or not spec["url"]:
|
|
223
|
+
raise core.UsageError(f"{source} cannot be synced on its own; it is fetched per query")
|
|
224
|
+
data = ctx.cache.get(source, spec["url"], ttl="0s" if args.force else None)
|
|
225
|
+
results.append({"id": source, "bytes": len(data) if data else 0})
|
|
226
|
+
degraded = [item for item in ctx.cache.report if item["status"] == "degraded"]
|
|
227
|
+
out = {"schema": "maggie-security-feeds.v1", "synced": results, "report": ctx.cache.report}
|
|
228
|
+
if degraded:
|
|
229
|
+
print(json.dumps(out, indent=2))
|
|
230
|
+
raise core.DegradedError(", ".join(d["id"] for d in degraded) + " unavailable")
|
|
231
|
+
return out
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def ledger_cmd(ctx: core.Context) -> dict:
|
|
235
|
+
args = ctx.args
|
|
236
|
+
ledger = core.Ledger(ctx.state.path("ledger.jsonl"))
|
|
237
|
+
if args.action == "list":
|
|
238
|
+
items = []
|
|
239
|
+
for fid, events in ledger.history().items():
|
|
240
|
+
state = core.Ledger.derive(events)
|
|
241
|
+
if args.status and state["status"] != args.status:
|
|
242
|
+
continue
|
|
243
|
+
items.append({"id": fid, **state})
|
|
244
|
+
return {"schema": "maggie-security-ledger.v1", "items": items}
|
|
245
|
+
if args.action == "compact":
|
|
246
|
+
return ledger.compact()
|
|
247
|
+
if not args.id:
|
|
248
|
+
raise core.UsageError(f"ledger {args.action} needs --id")
|
|
249
|
+
if args.action == "show":
|
|
250
|
+
events = ledger.history().get(args.id)
|
|
251
|
+
if not events:
|
|
252
|
+
raise core.UsageError(f"unknown finding id {args.id}")
|
|
253
|
+
return {"id": args.id, **core.Ledger.derive(events), "events": events}
|
|
254
|
+
event = {"accept": "accepted-risk", "false-positive": "false-positive", "reopen": "reopened"}[args.action]
|
|
255
|
+
until = args.until
|
|
256
|
+
if event == "accepted-risk" and not until:
|
|
257
|
+
until = core.iso(core.utcnow() + core.parse_duration("90d"))[:10]
|
|
258
|
+
return ledger.mark(args.id, event, args.reason, until=until if event == "accepted-risk" else None)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
HOUSEKEEPING = [
|
|
262
|
+
core.Command("doctor", "what applies to this project, tool and feed readiness, ownership, consents, ledger",
|
|
263
|
+
[], lambda p: p.add_argument("--fix", action="store_true", help="create config.json and the .gitignore entry"),
|
|
264
|
+
doctor, findings=False),
|
|
265
|
+
core.Command("tools", "pinned external scanners: list, install, verify", ["github-releases"],
|
|
266
|
+
lambda p: (p.add_argument("action", choices=["list", "install", "verify"]), p.add_argument("--tool")),
|
|
267
|
+
tools_cmd, findings=False),
|
|
268
|
+
core.Command("feeds", "local feed cache: status, sync, verify; mirror builds an offline copy of every feed", list(core.SOURCES),
|
|
269
|
+
lambda p: (p.add_argument("action", choices=["status", "sync", "verify", "mirror"]), p.add_argument("--source"),
|
|
270
|
+
p.add_argument("--force", action="store_true", help="ignore TTL"),
|
|
271
|
+
p.add_argument("--dir", help="mirror: target directory"),
|
|
272
|
+
p.add_argument("--ecosystems", help="mirror: OSV ecosystems to download (default the common eight)"),
|
|
273
|
+
p.add_argument("--days", type=int, default=7, help="mirror: days of CVE delta zips"),
|
|
274
|
+
p.add_argument("--to-project", action="store_true", help="mirror: also place OSV zips in .maggie/security/db/osv")),
|
|
275
|
+
feeds_cmd, findings=False),
|
|
276
|
+
core.Command("ledger", "finding status history (local marks only)", [],
|
|
277
|
+
lambda p: (p.add_argument("action", choices=["list", "show", "accept", "false-positive", "reopen", "compact"]),
|
|
278
|
+
p.add_argument("--id"), p.add_argument("--reason"), p.add_argument("--until", help="YYYY-MM-DD for accept (default +90d)"),
|
|
279
|
+
p.add_argument("--status", choices=core.STATUSES)),
|
|
280
|
+
ledger_cmd, findings=False),
|
|
281
|
+
]
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def build_parser(commands: dict[str, core.Command]) -> argparse.ArgumentParser:
|
|
285
|
+
parser = argparse.ArgumentParser(prog="maggie security", description="Maggie security checks (docs/maggie-security).")
|
|
286
|
+
sub = parser.add_subparsers(dest="command", required=True, metavar="<command>")
|
|
287
|
+
common = core.common_parent()
|
|
288
|
+
for command in commands.values():
|
|
289
|
+
p = sub.add_parser(command.name, parents=[common], help=command.help, description=command.help)
|
|
290
|
+
command.configure(p)
|
|
291
|
+
return parser
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def main(argv: list[str] | None = None) -> int:
|
|
295
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
296
|
+
commands = {c.name: c for c in HOUSEKEEPING}
|
|
297
|
+
commands.update(area_commands())
|
|
298
|
+
if argv and argv[0] in PLANNED and argv[0] not in commands:
|
|
299
|
+
print(f"maggie security {argv[0]} is planned for {PLANNED[argv[0]]} and not built yet", file=sys.stderr)
|
|
300
|
+
return core.EXIT_USAGE
|
|
301
|
+
parser = build_parser(commands)
|
|
302
|
+
try:
|
|
303
|
+
args = parser.parse_args(argv)
|
|
304
|
+
except SystemExit as error:
|
|
305
|
+
return core.EXIT_OK if error.code == 0 else core.EXIT_USAGE
|
|
306
|
+
command = commands[args.command]
|
|
307
|
+
if args.explain_egress:
|
|
308
|
+
return core.explain_egress(command.sources, command.name)
|
|
309
|
+
try:
|
|
310
|
+
ctx = core.Context(args, command.name, command.sources)
|
|
311
|
+
if args.offline:
|
|
312
|
+
with core.socket_guard():
|
|
313
|
+
result = command.run(ctx)
|
|
314
|
+
else:
|
|
315
|
+
result = command.run(ctx)
|
|
316
|
+
if isinstance(result, dict) and "_raw" in result:
|
|
317
|
+
if result["_raw"] is not None:
|
|
318
|
+
print(result["_raw"])
|
|
319
|
+
return core.EXIT_OK
|
|
320
|
+
if not command.findings:
|
|
321
|
+
if isinstance(result, dict) and "_raw" in result:
|
|
322
|
+
if result["_raw"] is not None:
|
|
323
|
+
print(result["_raw"])
|
|
324
|
+
return core.EXIT_OK
|
|
325
|
+
fmt = core.output_format(args)
|
|
326
|
+
if fmt == "text" and command.name == "doctor":
|
|
327
|
+
print(doctor_text(result))
|
|
328
|
+
elif fmt == "md" and isinstance(result, dict) and "markdown" in result:
|
|
329
|
+
print(result["markdown"])
|
|
330
|
+
else:
|
|
331
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
332
|
+
return result.get("exit_code", core.EXIT_OK) if isinstance(result, dict) else core.EXIT_OK
|
|
333
|
+
if isinstance(result, int):
|
|
334
|
+
return result
|
|
335
|
+
return core.emit(ctx, result)
|
|
336
|
+
except core.SecurityError as error:
|
|
337
|
+
print(f"maggie security {args.command}: {error}", file=sys.stderr)
|
|
338
|
+
return error.code
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
if __name__ == "__main__":
|
|
342
|
+
sys.exit(main())
|