Skip to content

Commit 23cb9b6

Browse files
authored
Create cli.py
1 parent bd6d531 commit 23cb9b6

1 file changed

Lines changed: 131 additions & 0 deletions

File tree

src/nod/cli.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import argparse
2+
import sys
3+
import os
4+
import json
5+
from .config import load_rules, load_ignore
6+
from .scanner import Scanner, SEVERITY_MAP
7+
from .generator import gen_template, gen_context, apply_fix
8+
from .reporters import gen_sarif, gen_report
9+
from .security import sign_attestation, freeze, verify
10+
from .utils import Colors, colorize
11+
12+
def main():
13+
parser = argparse.ArgumentParser(description="nod: AI Spec Compliance")
14+
parser.add_argument("path", nargs="?", help="File/Dir to audit")
15+
parser.add_argument("--rules", action='append')
16+
parser.add_argument("--init", action="store_true")
17+
parser.add_argument("--fix", action="store_true")
18+
parser.add_argument("--export", nargs="?", const="context", choices=["context", "cursor", "windsurf"], help="Export context/rules")
19+
parser.add_argument("--strict", action="store_true")
20+
parser.add_argument("--freeze", action="store_true")
21+
parser.add_argument("--verify", action="store_true")
22+
parser.add_argument("--min-severity", default="HIGH", choices=["MEDIUM", "HIGH", "CRITICAL"])
23+
parser.add_argument("--output", choices=["text", "json", "sarif", "compliance"], default="text")
24+
parser.add_argument("--save-to")
25+
args = parser.parse_args()
26+
27+
default_rules = ["defaults"] if os.path.isdir("defaults") else ["rules.yaml"]
28+
sources = args.rules if args.rules else default_rules
29+
30+
# Init config
31+
config = load_rules(sources)
32+
policy_version = config.get("version", "unknown")
33+
ignored = load_ignore(".nodignore")
34+
35+
if args.export:
36+
print(gen_context(config, policy_version, ignored, args.export))
37+
sys.exit(0)
38+
39+
if args.init:
40+
template = gen_template(config, policy_version)
41+
if args.path:
42+
if os.path.exists(args.path):
43+
print("Error: File exists", file=sys.stderr)
44+
sys.exit(1)
45+
with open(args.path, "w", encoding="utf-8") as f:
46+
f.write(template)
47+
print(f"✅ Generated: {args.path}")
48+
else:
49+
print(template)
50+
sys.exit(0)
51+
52+
if not args.path:
53+
parser.print_help()
54+
sys.exit(1)
55+
56+
scanner = Scanner(config, ignored)
57+
results, max_sev_label = scanner.scan_input(args.path, strict=args.strict, version=policy_version)
58+
59+
# Sign attestation
60+
sign_attestation(scanner.attestation)
61+
62+
if args.freeze:
63+
freeze(policy_version, scanner.attestation)
64+
sys.exit(0)
65+
66+
if args.verify:
67+
if not verify(scanner.attestation):
68+
sys.exit(1)
69+
sys.exit(0)
70+
71+
if args.fix:
72+
apply_fix(args.path, results)
73+
sys.exit(0)
74+
75+
output_content = ""
76+
exit_code = 0
77+
78+
if args.output == "sarif":
79+
output_content = json.dumps(gen_sarif(scanner.attestation, args.path), indent=2)
80+
elif args.output == "json":
81+
output_content = json.dumps(scanner.attestation, indent=2)
82+
elif args.output == "compliance":
83+
output_content = gen_report(scanner.attestation)
84+
else:
85+
summary = [f"\n--- nod Summary ---\nTarget: {args.path}\nMax Sev: {max_sev_label}"]
86+
if scanner.attestation.get("signed"):
87+
summary.append(f"{colorize('🔒 Signed', Colors.GREEN)}")
88+
89+
fail_check = False
90+
min_val = SEVERITY_MAP.get(args.min_severity, 0)
91+
92+
for data in results.values():
93+
summary.append(f"\n[{colorize(data['label'], Colors.BOLD)}]")
94+
for check in data["checks"]:
95+
name = check.get("label") or check['id']
96+
if check["status"] == "FAIL":
97+
sev_col = Colors.RED if check['severity'] in ["CRITICAL", "HIGH"] else Colors.YELLOW
98+
summary.append(f" {colorize('❌', Colors.RED)} [{colorize(check['severity'], sev_col)}] {name}")
99+
if check.get("source"):
100+
summary.append(f" File: {check['source']}")
101+
102+
if SEVERITY_MAP.get(check["severity"], 0) >= min_val:
103+
fail_check = True
104+
elif check["status"] == "EXCEPTION":
105+
summary.append(f" {colorize('⚪', Colors.BLUE)} [EXCEPTION] {name}")
106+
elif check["status"] == "SKIPPED":
107+
summary.append(f" {colorize('⏭️', Colors.CYAN)} [SKIPPED] {name}")
108+
else:
109+
summary.append(f" {colorize('✅', Colors.GREEN)} [PASS] {name}")
110+
111+
status_msg = f"\nFAIL: Blocked by {args.min_severity}+" if fail_check else "\nPASS: Nod granted."
112+
summary.append(colorize(status_msg, Colors.RED if fail_check else Colors.GREEN))
113+
output_content = "\n".join(summary)
114+
if fail_check:
115+
exit_code = 1
116+
117+
if SEVERITY_MAP.get(max_sev_label, 0) >= SEVERITY_MAP.get(args.min_severity, 0):
118+
exit_code = 1
119+
120+
if args.save_to:
121+
try:
122+
with open(args.save_to, "w", encoding="utf-8") as f:
123+
f.write(output_content)
124+
print(f"Saved: {args.save_to}")
125+
except Exception as e:
126+
print(f"Error saving file: {e}", file=sys.stderr)
127+
sys.exit(1)
128+
else:
129+
print(output_content)
130+
131+
sys.exit(exit_code)

0 commit comments

Comments
 (0)