-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcli.py
More file actions
104 lines (93 loc) · 3.8 KB
/
Copy pathcli.py
File metadata and controls
104 lines (93 loc) · 3.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
from __future__ import annotations
import argparse
import json
import sys
from collections import Counter
from .exporters import ExportError, export_redreport
from .parser import SpecError, load_inventory
from .rules import scan_inventory
SEVERITY_ORDER = {"critical": 4, "high": 3, "medium": 2, "low": 1, "informational": 0}
def parser() -> argparse.ArgumentParser:
root = argparse.ArgumentParser(
prog="asm", description="Deterministic OpenAPI attack-surface mapper"
)
commands = root.add_subparsers(dest="command", required=True)
validate = commands.add_parser("validate", help="validate and parse an OpenAPI document")
validate.add_argument("spec")
inventory = commands.add_parser("inventory", help="list API operations and authentication")
inventory.add_argument("spec")
inventory.add_argument("--json", action="store_true")
scan = commands.add_parser("scan", help="analyze deterministic API security signals")
scan.add_argument("spec")
scan.add_argument("--json", action="store_true")
scan.add_argument("--fail-on", choices=list(SEVERITY_ORDER))
export = commands.add_parser("export", help="export findings")
export.add_argument("format", choices=["redreport"])
export.add_argument("spec")
export.add_argument("--output", "-o", required=True)
export.add_argument("--client", default="Portfolio Lab")
return root
def main(argv: list[str] | None = None) -> int:
args = parser().parse_args(argv)
try:
inventory = load_inventory(args.spec)
except SpecError as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 2
if args.command == "validate":
print(
f"VALID: title={inventory.title!r} openapi={inventory.openapi} "
f"endpoints={len(inventory.endpoints)} servers={len(inventory.servers)}"
)
return 0
if args.command == "inventory":
values = [
{
"method": endpoint.method,
"path": endpoint.path,
"operation_id": endpoint.operation_id,
"anonymous": endpoint.anonymous,
"auth_schemes": endpoint.auth_schemes,
"parameters": len(endpoint.parameters),
}
for endpoint in inventory.endpoints
]
if args.json:
print(json.dumps(values, indent=2, ensure_ascii=False))
else:
for endpoint in inventory.endpoints:
auth = "anonymous" if endpoint.anonymous else ",".join(endpoint.auth_schemes)
print(f"{endpoint.method:7} {endpoint.path:40} auth={auth}")
return 0
findings = scan_inventory(inventory)
if args.command == "scan":
if args.json:
print(json.dumps([finding.as_dict() for finding in findings], indent=2))
else:
for finding in findings:
print(
f"{finding.severity.upper():6} {finding.rule_id} "
f"{finding.asset} — {finding.title}"
)
counts = Counter(finding.severity for finding in findings)
print(
"SUMMARY "
+ " ".join(f"{severity}={counts[severity]}" for severity in SEVERITY_ORDER)
)
if args.fail_on:
threshold = SEVERITY_ORDER[args.fail_on]
if any(SEVERITY_ORDER[finding.severity] >= threshold for finding in findings):
return 3
return 0
if args.command == "export":
try:
files = export_redreport(inventory, findings, args.output, args.client)
except ExportError as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 2
for path in files:
print(f"EXPORTED: {path.resolve()}")
return 0
return 1
if __name__ == "__main__":
raise SystemExit(main())