agentsec-cli 0.1.0__py3-none-any.whl

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.
agentsec/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """AgentSec: Static security scanner for AI coding agents and MCP configs."""
2
+
3
+ __version__ = "0.1.0"
agentsec/baseline.py ADDED
@@ -0,0 +1,60 @@
1
+ """Baseline (lockfile) management for AgentSec."""
2
+
3
+ import json
4
+ import hashlib
5
+ from pathlib import Path
6
+ from typing import Dict, List, Tuple
7
+
8
+
9
+ def compute_finding_id(finding: dict) -> str:
10
+ """Compute a stable unique ID for a finding based on rule, file, and server."""
11
+ key = f"{finding['rule']}|{finding['file']}|{finding.get('server', '')}"
12
+ return hashlib.md5(key.encode()).hexdigest()
13
+
14
+
15
+ def load_baseline(path: str) -> Dict[str, str]:
16
+ """Load baseline from JSON file. Returns dict of id -> severity."""
17
+ p = Path(path)
18
+ if not p.exists():
19
+ return {}
20
+ with open(p, 'r') as f:
21
+ data = json.load(f)
22
+ return data.get('findings', {})
23
+
24
+
25
+ def save_baseline(path: str, findings: List[dict]) -> None:
26
+ """Save current findings as baseline."""
27
+ baseline = {}
28
+ for f in findings:
29
+ fid = compute_finding_id(f)
30
+ baseline[fid] = f['severity']
31
+ with open(path, 'w') as f:
32
+ json.dump({"findings": baseline}, f, indent=2)
33
+
34
+
35
+ def compare_findings(findings: List[dict], baseline: Dict[str, str]) -> Tuple[List[dict], List[dict], List[dict]]:
36
+ """
37
+ Compare current findings against baseline.
38
+ Returns: (new, changed, removed)
39
+ - new: findings not in baseline
40
+ - changed: findings whose severity changed
41
+ - removed: baseline entries not in current findings (as list of dict with 'id' and 'severity')
42
+ """
43
+ current_ids = set()
44
+ new = []
45
+ changed = []
46
+
47
+ for f in findings:
48
+ fid = compute_finding_id(f)
49
+ current_ids.add(fid)
50
+ if fid not in baseline:
51
+ new.append(f)
52
+ elif baseline[fid] != f['severity']:
53
+ changed.append(f)
54
+
55
+ removed = []
56
+ for fid, sev in baseline.items():
57
+ if fid not in current_ids:
58
+ removed.append({'id': fid, 'severity': sev})
59
+
60
+ return new, changed, removed
agentsec/cli.py ADDED
@@ -0,0 +1,84 @@
1
+ """CLI entry point for AgentSec."""
2
+
3
+ import sys
4
+ import click
5
+ from pathlib import Path
6
+ from .scanner import Scanner
7
+ from .report import print_summary
8
+ from .baseline import load_baseline, save_baseline, compare_findings, compute_finding_id
9
+ from .owasp import format_owasp
10
+
11
+
12
+ @click.group()
13
+ def cli():
14
+ """AgentSec — security scanner for AI agent configs."""
15
+ pass
16
+
17
+
18
+ @cli.command()
19
+ @click.argument("path", default=".", type=click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True))
20
+ @click.option("--format", "-f", default="terminal", help="Output format: terminal, json, markdown, sarif")
21
+ @click.option("--severity", default="all", help="Minimum severity: critical, high, medium, low, all")
22
+ @click.option("--fail-on", type=click.Choice(["critical", "high", "medium", "low"], case_sensitive=False), help="Exit with code 1 if any finding is at least this severity")
23
+ @click.option("--include-hidden", is_flag=True, help="Include hidden files and directories")
24
+ @click.option("--baseline", type=click.Path(exists=True, dir_okay=False, resolve_path=True), help="Path to baseline JSON file (lockfile). Compare findings against it.")
25
+ @click.option("--update-baseline", type=click.Path(dir_okay=False, resolve_path=True), help="Save current findings as baseline JSON file and exit.")
26
+ @click.option("--show-owasp", is_flag=True, default=False, help="Show OWASP Top 10 for LLM mapping IDs for each finding")
27
+ def scan(path, format, severity, include_hidden, fail_on=None, baseline=None, update_baseline=None, show_owasp=False):
28
+ """Scan a directory for security risks in AI agent configurations."""
29
+ if format == "terminal":
30
+ click.echo(f" Scanning {path}...")
31
+ scanner = Scanner(Path(path), include_hidden=include_hidden, min_severity=severity)
32
+ findings = scanner.scan()
33
+
34
+ # If update-baseline is provided, save baseline and exit
35
+ if update_baseline:
36
+ save_baseline(update_baseline, findings)
37
+ if format == "terminal":
38
+ click.echo(f" Baseline saved to {update_baseline}")
39
+ return
40
+
41
+ # Load baseline if provided
42
+ if baseline:
43
+ baseline_findings = load_baseline(baseline)
44
+ new, changed, removed = compare_findings(findings, baseline_findings)
45
+ if format == "terminal":
46
+ click.echo(f"\n Baseline comparison against {baseline}:")
47
+ click.echo(f" New findings: {len(new)}")
48
+ click.echo(f" Changed severity: {len(changed)}")
49
+ click.echo(f" Removed findings: {len(removed)}")
50
+ if new:
51
+ click.echo("\n New findings:")
52
+ for f in new:
53
+ click.echo(f" [{f['severity']}] {f['rule']} ({f['file']})")
54
+ if changed:
55
+ click.echo("\n Changed findings:")
56
+ for f in changed:
57
+ old_sev = baseline_findings.get(compute_finding_id(f), 'unknown')
58
+ click.echo(f" [{old_sev} -> {f['severity']}] {f['rule']} ({f['file']})")
59
+ if removed:
60
+ click.echo("\n Removed findings (baseline only):")
61
+ for r in removed:
62
+ click.echo(f" [{r['severity']}] id: {r['id']}")
63
+ print_summary(findings, format, show_owasp=show_owasp)
64
+ if new or changed:
65
+ sys.exit(1)
66
+ else:
67
+ # Add OWASP tags to terminal output header if show_owasp
68
+ if format == "terminal" and show_owasp:
69
+ click.echo(" OWASP mapping enabled (LLM = OWASP Top 10 for LLM, AG = OWASP Agentic Security)\n")
70
+ print_summary(findings, format, show_owasp=show_owasp)
71
+
72
+ # Existing fail-on logic
73
+ if fail_on:
74
+ severity_levels = {"low": 0, "medium": 1, "high": 2, "critical": 3}
75
+ min_fail = severity_levels.get(fail_on.lower(), -1)
76
+ if min_fail >= 0:
77
+ for f in findings:
78
+ if severity_levels.get(f["severity"].lower(), -1) >= min_fail:
79
+ click.echo(f"Failing due to {f['severity']} finding: {f['rule']}")
80
+ sys.exit(1)
81
+
82
+
83
+ if __name__ == "__main__":
84
+ cli()
agentsec/owasp.py ADDED
@@ -0,0 +1,212 @@
1
+ """OWASP Top 10 for LLM Applications — rule mapping for AgentSec.
2
+
3
+ Maps each AgentSec rule to the relevant OWASP categories.
4
+
5
+ OWASP Top 10 for LLM Applications (2025):
6
+ https://genai.owasp.org/
7
+
8
+ LLM01 — Prompt Injection
9
+ LLM02 — Sensitive Information Disclosure
10
+ LLM03 — Supply Chain Vulnerabilities
11
+ LLM04 — Data Leakage via External Services
12
+ LLM05 — Insecure Output Handling
13
+ LLM06 — Excessive Agency / Unrestricted Autonomy
14
+ LLM07 — Insecure Plugin / Extension Design
15
+ LLM08 — Excessive Permissions / Overprivileged Access
16
+ LLM09 — Over-reliance / Insufficient Oversight
17
+ LLM10 — Model Theft / Intellectual Property Loss
18
+
19
+ OWASP Agentic Security Top 10 (2025):
20
+ AG01 — Insecure Agent-to-Agent Communication
21
+ AG02 — Unauthorized Tool Access
22
+ AG03 — Agent Impersonation
23
+ AG04 — Task Delegation Abuse
24
+ AG05 — Memory / Prompt Leakage
25
+ AG06 — Inconsistent Authorization
26
+ AG07 — Output Validation Failure
27
+ AG08 — Agent Workflow Manipulation
28
+ AG09 — Inadequate Audit Trail
29
+ AG10 — Privilege Escalation
30
+ """
31
+
32
+ # Mapping: OWASP ID -> (short name, description)
33
+ OWASP_CATEGORIES = {
34
+ "LLM01": ("Prompt Injection", "Attacker injects malicious instructions via user prompts or indirect inputs"),
35
+ "LLM02": ("Sensitive Information Disclosure", "LLM or agent exposes sensitive data (secrets, PII, internal info)"),
36
+ "LLM03": ("Supply Chain Vulnerabilities", "Compromised dependencies, packages, or third-party components"),
37
+ "LLM04": ("Data Leakage via External Services", "Sensitive data exfiltrated through network calls, APIs, or integrations"),
38
+ "LLM05": ("Insecure Output Handling", "Agent output is not validated before being used in downstream operations"),
39
+ "LLM06": ("Excessive Agency", "Agent has more autonomy than necessary — can perform actions without oversight"),
40
+ "LLM07": ("Insecure Plugin/Extension Design", "Plugins or extensions have weak security boundaries"),
41
+ "LLM08": ("Excessive Permissions", "Agent or tool has overly broad filesystem/network/OS permissions"),
42
+ "LLM09": ("Over-reliance / Insufficient Oversight", "No guardrails, audit, or policy enforcement for agent actions"),
43
+ "LLM10": ("Model Theft / IP Loss", "Risk of proprietary model weights or IP extraction"),
44
+ # Agentic Security
45
+ "AG01": ("Insecure Agent-to-Agent Communication", "Agents communicate without proper authentication or encryption"),
46
+ "AG02": ("Unauthorized Tool Access", "Agent can invoke tools or capabilities without proper authorization gates"),
47
+ "AG03": ("Agent Impersonation", "Attacker impersonates a legitimate agent to gain access"),
48
+ "AG04": ("Task Delegation Abuse", "Malicious tasks can be delegated to sub-agents without validation"),
49
+ "AG05": ("Memory/Prompt Leakage", "Agent memory or prompt context can leak across sessions or users"),
50
+ "AG06": ("Inconsistent Authorization", "Authorization policies are missing, incomplete, or not enforced"),
51
+ "AG07": ("Output Validation Failure", "Agent output is not validated for safety or correctness"),
52
+ "AG08": ("Agent Workflow Manipulation", "Attacker manipulates the agent's workflow or decision chain"),
53
+ "AG09": ("Inadequate Audit Trail", "Agent actions are not logged or traceable"),
54
+ "AG10": ("Privilege Escalation", "Agent can escalate its own privileges beyond intended scope"),
55
+ }
56
+
57
+ # Rule-to-OWASP mapping: rule_name -> list of (owasp_id, category_name)
58
+ # Covers all 41 rules from base.py + additional.py (deduplicated)
59
+ RULE_OWASP_MAP = {
60
+ # === Base rules ===
61
+ "MCP shell execution": [
62
+ ("LLM06", "Excessive Agency"),
63
+ ("AG02", "Unauthorized Tool Access"),
64
+ ],
65
+ "MCP filesystem write access": [
66
+ ("LLM08", "Excessive Permissions"),
67
+ ],
68
+ "Secret exposure": [
69
+ ("LLM02", "Sensitive Information Disclosure"),
70
+ ],
71
+ "Broad path access": [
72
+ ("LLM08", "Excessive Permissions"),
73
+ ],
74
+ "Prompt injection risk": [
75
+ ("LLM01", "Prompt Injection"),
76
+ ],
77
+ "Sensitive file reference": [
78
+ ("LLM02", "Sensitive Information Disclosure"),
79
+ ],
80
+ "Excessive autonomy": [
81
+ ("LLM06", "Excessive Agency"),
82
+ ],
83
+ "Unpinned dependency": [
84
+ ("LLM03", "Supply Chain Vulnerabilities"),
85
+ ],
86
+ "Remote script install": [
87
+ ("LLM03", "Supply Chain Vulnerabilities"),
88
+ ],
89
+ "Docker socket access": [
90
+ ("LLM08", "Excessive Permissions"),
91
+ ("AG02", "Unauthorized Tool Access"),
92
+ ],
93
+ # === Additional rules ===
94
+ "Network + filesystem access": [
95
+ ("LLM04", "Data Leakage via External Services"),
96
+ ("AG05", "Memory/Prompt Leakage"),
97
+ ],
98
+ "Suspicious tool description": [
99
+ ("LLM01", "Prompt Injection"),
100
+ ("AG10", "Privilege Escalation"),
101
+ ],
102
+ "GitHub token exposure": [
103
+ ("LLM02", "Sensitive Information Disclosure"),
104
+ ("AG02", "Unauthorized Tool Access"),
105
+ ],
106
+ "Communication tool write permission": [
107
+ ("LLM04", "Data Leakage via External Services"),
108
+ ],
109
+ "Database write/delete permission": [
110
+ ("LLM08", "Excessive Permissions"),
111
+ ],
112
+ "Excessive autonomy instruction": [
113
+ ("LLM06", "Excessive Agency"),
114
+ ],
115
+ "Prompt injection in markdown": [
116
+ ("LLM01", "Prompt Injection"),
117
+ ],
118
+ "MCP OAuth broad scopes": [
119
+ ("LLM07", "Insecure Plugin/Extension Design"),
120
+ ("AG06", "Inconsistent Authorization"),
121
+ ],
122
+ "Web + filesystem access": [
123
+ ("LLM04", "Data Leakage via External Services"),
124
+ ("LLM08", "Excessive Permissions"),
125
+ ],
126
+ "Read repo + network": [
127
+ ("LLM04", "Data Leakage via External Services"),
128
+ ("AG05", "Memory/Prompt Leakage"),
129
+ ],
130
+ "Unknown/untrusted source": [
131
+ ("LLM03", "Supply Chain Vulnerabilities"),
132
+ ],
133
+ "No policy file": [
134
+ ("LLM09", "Over-reliance / Insufficient Oversight"),
135
+ ("AG06", "Inconsistent Authorization"),
136
+ ],
137
+ "Cursor agent config with dangerous permissions": [
138
+ ("LLM08", "Excessive Permissions"),
139
+ ("AG02", "Unauthorized Tool Access"),
140
+ ],
141
+ "Claude Desktop config with MCP server risks": [
142
+ ("LLM07", "Insecure Plugin/Extension Design"),
143
+ ],
144
+ "Codex/Cline agent with unrestricted tools": [
145
+ ("LLM08", "Excessive Permissions"),
146
+ ("AG02", "Unauthorized Tool Access"),
147
+ ],
148
+ "Environment variable exposure": [
149
+ ("LLM02", "Sensitive Information Disclosure"),
150
+ ],
151
+ "Vulnerable dependency pattern": [
152
+ ("LLM03", "Supply Chain Vulnerabilities"),
153
+ ],
154
+ "Insecure default command": [
155
+ ("LLM07", "Insecure Plugin/Extension Design"),
156
+ ],
157
+ "Read-only file system in MCP server": [
158
+ ("LLM08", "Excessive Permissions"),
159
+ ],
160
+ "Missing input validation": [
161
+ ("LLM05", "Insecure Output Handling"),
162
+ ("AG07", "Output Validation Failure"),
163
+ ],
164
+ "Package manager execution": [
165
+ ("LLM03", "Supply Chain Vulnerabilities"),
166
+ ],
167
+ "Container privileged mode": [
168
+ ("LLM08", "Excessive Permissions"),
169
+ ("AG02", "Unauthorized Tool Access"),
170
+ ],
171
+ "Host mount exposure": [
172
+ ("LLM08", "Excessive Permissions"),
173
+ ],
174
+ "Browser automation with local file access": [
175
+ ("LLM04", "Data Leakage via External Services"),
176
+ ("AG05", "Memory/Prompt Leakage"),
177
+ ],
178
+ "Dynamic code execution": [
179
+ ("LLM06", "Excessive Agency"),
180
+ ("AG10", "Privilege Escalation"),
181
+ ],
182
+ "Wildcard tool allowlist": [
183
+ ("LLM08", "Excessive Permissions"),
184
+ ],
185
+ "Telemetry or analytics endpoint": [
186
+ ("LLM04", "Data Leakage via External Services"),
187
+ ],
188
+ "Credential helper access": [
189
+ ("LLM02", "Sensitive Information Disclosure"),
190
+ ("AG02", "Unauthorized Tool Access"),
191
+ ],
192
+ }
193
+
194
+
195
+ def get_owasp(rule_name: str) -> list:
196
+ """Return OWASP mappings for a rule name. Returns [(owasp_id, category_name), ...] or empty list."""
197
+ return RULE_OWASP_MAP.get(rule_name, [])
198
+
199
+
200
+ def get_owasp_ids(rule_name: str) -> str:
201
+ """Return OWASP IDs as a comma-separated string (e.g. 'LLM06, AG02')."""
202
+ mappings = get_owasp(rule_name)
203
+ return ", ".join(owasp_id for owasp_id, _ in mappings)
204
+
205
+
206
+ def format_owasp(rule_name: str) -> str:
207
+ """Format OWASP info for terminal output: '[LLM06, AG02]' or empty string."""
208
+ mappings = get_owasp(rule_name)
209
+ if not mappings:
210
+ return ""
211
+ ids = ", ".join(f"{owasp_id}" for owasp_id, _ in mappings)
212
+ return f"[{ids}]"
@@ -0,0 +1,5 @@
1
+ """File parsers for AgentSec."""
2
+
3
+ from .core import parse_file
4
+
5
+ __all__ = ["parse_file"]
@@ -0,0 +1,8 @@
1
+ from pathlib import Path
2
+
3
+ def parse_file(file_path: Path) -> str | None:
4
+ try:
5
+ with open(file_path, 'r', encoding='utf-8') as f:
6
+ return f.read()
7
+ except Exception:
8
+ return None
@@ -0,0 +1,69 @@
1
+ import json
2
+ from pathlib import Path
3
+ from typing import Any, Dict, List, Optional
4
+
5
+ def parse_mcp_config(content: str, file_path: Path) -> Optional[List[Dict[str, Any]]]:
6
+ """Parse MCP config from JSON content.
7
+
8
+ Expects a JSON object with an 'mcpServers' key or a direct list of servers.
9
+ Returns a list of server dicts with keys: name, command, args, env, capabilities.
10
+ """
11
+ try:
12
+ data = json.loads(content)
13
+ except json.JSONDecodeError:
14
+ return None
15
+
16
+ servers = []
17
+
18
+ # Check for mcpServers object
19
+ if isinstance(data, dict) and "mcpServers" in data:
20
+ mcp_servers = data["mcpServers"]
21
+ if isinstance(mcp_servers, dict):
22
+ for name, config in mcp_servers.items():
23
+ if isinstance(config, dict):
24
+ server = {
25
+ "name": name,
26
+ "command": config.get("command", ""),
27
+ "args": config.get("args", []),
28
+ "env": config.get("env", {}),
29
+ "capabilities": _infer_capabilities(config)
30
+ }
31
+ servers.append(server)
32
+ elif isinstance(data, list):
33
+ # Maybe a list of servers
34
+ for item in data:
35
+ if isinstance(item, dict) and "command" in item:
36
+ server = {
37
+ "name": item.get("name", ""),
38
+ "command": item.get("command", ""),
39
+ "args": item.get("args", []),
40
+ "env": item.get("env", {}),
41
+ "capabilities": _infer_capabilities(item)
42
+ }
43
+ servers.append(server)
44
+ return servers if servers else None
45
+
46
+ def _infer_capabilities(config: Dict[str, Any]) -> List[str]:
47
+ """Infer capabilities from command, args, and env."""
48
+ caps = []
49
+ text = str(config).lower()
50
+
51
+ # Filesystem
52
+ if "filesystem" in text or "write" in text or "edit" in text or "delete" in text or "rm" in text or "mv" in text:
53
+ caps.append("filesystem")
54
+ # Shell
55
+ if "bash" in text or "sh" in text or "powershell" in text or "cmd" in text or "exec" in text or "subprocess" in text or "terminal" in text or "run_command" in text:
56
+ caps.append("shell")
57
+ # Network
58
+ if "http" in text or "https" in text or "curl" in text or "wget" in text or "fetch" in text:
59
+ caps.append("network")
60
+ # Secrets
61
+ if "env" in text or ".env" in text or "secret" in text or "token" in text or "key" in text:
62
+ caps.append("secrets")
63
+ # Slack/email/github
64
+ if "slack" in text or "gmail" in text or "email" in text or "send_message" in text or "post_message" in text or "create_issue" in text or "comment" in text or "reply" in text:
65
+ caps.append("communication")
66
+ # Database
67
+ if "postgres" in text or "mysql" in text or "mongodb" in text or "redis" in text or "delete" in text or "drop" in text or "update" in text or "insert" in text:
68
+ caps.append("database")
69
+ return caps
@@ -0,0 +1,28 @@
1
+ import tomllib
2
+ from pathlib import Path
3
+ from typing import Any, Dict, List, Optional
4
+ from .json_parser import _infer_capabilities
5
+
6
+ def parse_mcp_config(content: str, file_path: Path) -> Optional[List[Dict[str, Any]]]:
7
+ """Parse MCP config from TOML content."""
8
+ try:
9
+ data = tomllib.loads(content)
10
+ except (tomllib.TOMLDecodeError, TypeError):
11
+ return None
12
+
13
+ if not isinstance(data, dict):
14
+ return None
15
+
16
+ servers = []
17
+ if "mcpServers" in data and isinstance(data["mcpServers"], dict):
18
+ for name, config in data["mcpServers"].items():
19
+ if isinstance(config, dict):
20
+ server = {
21
+ "name": name,
22
+ "command": config.get("command", ""),
23
+ "args": config.get("args", []),
24
+ "env": config.get("env", {}),
25
+ "capabilities": _infer_capabilities(config)
26
+ }
27
+ servers.append(server)
28
+ return servers if servers else None
@@ -0,0 +1,28 @@
1
+ import yaml
2
+ from pathlib import Path
3
+ from typing import Any, Dict, List, Optional
4
+ from .json_parser import _infer_capabilities
5
+
6
+ def parse_mcp_config(content: str, file_path: Path) -> Optional[List[Dict[str, Any]]]:
7
+ """Parse MCP config from YAML content."""
8
+ try:
9
+ data = yaml.safe_load(content)
10
+ except yaml.YAMLError:
11
+ return None
12
+
13
+ if not isinstance(data, dict):
14
+ return None
15
+
16
+ servers = []
17
+ if "mcpServers" in data and isinstance(data["mcpServers"], dict):
18
+ for name, config in data["mcpServers"].items():
19
+ if isinstance(config, dict):
20
+ server = {
21
+ "name": name,
22
+ "command": config.get("command", ""),
23
+ "args": config.get("args", []),
24
+ "env": config.get("env", {}),
25
+ "capabilities": _infer_capabilities(config)
26
+ }
27
+ servers.append(server)
28
+ return servers if servers else None
agentsec/report.py ADDED
@@ -0,0 +1,36 @@
1
+ """Output formatters for AgentSec findings."""
2
+
3
+ def print_summary(findings: list, format: str, show_owasp: bool = False) -> None:
4
+ """Print findings in the requested format."""
5
+ if format == "terminal":
6
+ for f in findings:
7
+ owasp_tag = f" {f.get('owasp', '')}" if show_owasp and f.get('owasp') else ""
8
+ print(f"[{f['severity'].upper()}]{owasp_tag} {f['rule']}")
9
+ print(f" File: {f['file']}")
10
+ if f.get('server'):
11
+ print(f" Server: {f['server']}")
12
+ print(f" Description: {f['description']}")
13
+ print(f" Recommendation: {f['recommendation']}")
14
+ if show_owasp and f.get('owasp'):
15
+ print(f" OWASP: {f['owasp']}")
16
+ print()
17
+ print(f"Total findings: {len(findings)}")
18
+ elif format == "json":
19
+ import json
20
+ print(json.dumps(findings, indent=2))
21
+ elif format == "markdown":
22
+ print("# AgentSec Report\n")
23
+ for f in findings:
24
+ owasp_tag = f" ({f.get('owasp')})" if show_owasp and f.get('owasp') else ""
25
+ print(f"## {f['severity'].upper()}: {f['rule']}{owasp_tag}")
26
+ print(f"**File:** {f['file']}")
27
+ if f.get('server'):
28
+ print(f"**Server:** {f['server']}")
29
+ print(f"**Description:** {f['description']}")
30
+ print(f"**Recommendation:** {f['recommendation']}")
31
+ if show_owasp and f.get('owasp'):
32
+ print(f"**OWASP:** {f['owasp']}")
33
+ print()
34
+ elif format == "sarif":
35
+ from .sarif import print_sarif
36
+ print_sarif(findings)
@@ -0,0 +1,5 @@
1
+ """Security rules for AgentSec."""
2
+
3
+ from .base import Rule, load_rules
4
+
5
+ __all__ = ["Rule", "load_rules"]