iris-security-cli 0.2.5__tar.gz → 0.2.12__tar.gz
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.
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/PKG-INFO +3 -3
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/certify.py +165 -5
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/compliance_check_cmd.py +33 -12
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/declare.py +3 -0
- iris_security_cli-0.2.12/iris_cli/discover.py +223 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/evidence.py +363 -1
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/framework_suggest.py +36 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/license_cmd.py +12 -6
- iris_security_cli-0.2.12/iris_cli/list_cmd.py +283 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/main.py +38 -2
- iris_security_cli-0.2.12/iris_cli/onboarding_report.py +46 -0
- iris_security_cli-0.2.12/iris_cli/org_policy.py +399 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/policy_cache.py +9 -0
- iris_security_cli-0.2.12/iris_cli/policy_commit.py +129 -0
- iris_security_cli-0.2.12/iris_cli/policy_status.py +147 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/quickstart.py +2 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_security_cli.egg-info/PKG-INFO +3 -3
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_security_cli.egg-info/SOURCES.txt +8 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_security_cli.egg-info/requires.txt +2 -2
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/pyproject.toml +3 -3
- iris_security_cli-0.2.12/tests/test_evidence_query.py +210 -0
- iris_security_cli-0.2.12/tests/test_list_cmd.py +94 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/README.md +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/__init__.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/action_plan.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/assess.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/cedar_parser.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/compiler_config.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/compliance_fix.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/cost.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/delegation.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/dlp_cmd.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/drift.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/enforce.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/entitlements_cmd.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/explain.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/framework_test.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/hitl.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/mcp_server.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/models_cmd.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/policy_diff.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/preview.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/redteam.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/regulatory.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/scan_govern.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/scan_report.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/scm.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/sentinel.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/status_cmd.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/users.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/vault.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/watch.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_cli/witness.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_security_cli.egg-info/dependency_links.txt +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_security_cli.egg-info/entry_points.txt +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/iris_security_cli.egg-info/top_level.txt +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/setup.cfg +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/tests/test_entitlements_display.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/tests/test_evidence.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/tests/test_framework_suggest.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/tests/test_framework_test.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/tests/test_hitl_cli.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/tests/test_mcp_model_governance.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/tests/test_policy_diff.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/tests/test_quickstart.py +0 -0
- {iris_security_cli-0.2.5 → iris_security_cli-0.2.12}/tests/test_vocabulary.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: iris-security-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.12
|
|
4
4
|
Summary: IRIS CLI — declare, compile, preview, enforce, witness, certify, sentinel
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Project-URL: Homepage, https://github.com/gimartinb/iris-sdk
|
|
@@ -15,8 +15,8 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.12
|
|
16
16
|
Requires-Python: >=3.10
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
|
-
Requires-Dist: iris-security-core>=0.1.
|
|
19
|
-
Requires-Dist: iris-security-sdk>=0.2.
|
|
18
|
+
Requires-Dist: iris-security-core>=0.1.16
|
|
19
|
+
Requires-Dist: iris-security-sdk>=0.2.12
|
|
20
20
|
Requires-Dist: click>=8.1
|
|
21
21
|
Requires-Dist: rich>=13.0
|
|
22
22
|
Requires-Dist: pyyaml>=6.0
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import json
|
|
5
6
|
from datetime import datetime
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from typing import Optional
|
|
@@ -9,8 +10,17 @@ from typing import Optional
|
|
|
9
10
|
import click
|
|
10
11
|
from rich.console import Console
|
|
11
12
|
from rich.panel import Panel
|
|
13
|
+
from rich.table import Table
|
|
12
14
|
|
|
13
15
|
from iris_core.compliance.dynamic_loader import ComplianceRule, DynamicBundleLoader
|
|
16
|
+
from iris_core.compliance.exporters.aiuc1_export import AIUC1EvidenceExporter
|
|
17
|
+
from iris_core.compliance.framework_check import load_bundle_data, run_framework_check
|
|
18
|
+
from iris_core.compliance.bundles.iso42001 import (
|
|
19
|
+
ISO42001_BUNDLE,
|
|
20
|
+
ISO42001_CLAUSES,
|
|
21
|
+
coverage_breakdown,
|
|
22
|
+
stale_crosswalk_warning,
|
|
23
|
+
)
|
|
14
24
|
from iris_core.entitlements import Entitlements, Feature
|
|
15
25
|
from iris_core.models.passport import AgentPassport
|
|
16
26
|
|
|
@@ -26,6 +36,12 @@ from iris_cli.framework_test import (
|
|
|
26
36
|
|
|
27
37
|
console = Console()
|
|
28
38
|
|
|
39
|
+
_CERTIFY_FRAMEWORKS = sorted([
|
|
40
|
+
"colorado-ai-act", "nist-ai-rmf", "fedramp-moderate", "hipaa", "soc2", "gdpr",
|
|
41
|
+
"nyc-ll144", "illinois-ai-video", "eu-ai-act", "ccpa-admt", "china-pipl",
|
|
42
|
+
"aiuc-1", "iso-42001",
|
|
43
|
+
])
|
|
44
|
+
|
|
29
45
|
|
|
30
46
|
def _evaluate_custom_rule(rule: ComplianceRule, passport: AgentPassport) -> str:
|
|
31
47
|
safe_globals = {"__builtins__": {}}
|
|
@@ -120,17 +136,116 @@ def _render_custom_rules_section(
|
|
|
120
136
|
console.print(f" Fix: {rule.remediation_command}")
|
|
121
137
|
|
|
122
138
|
|
|
139
|
+
def _render_iso42001_certification(agent_name: str) -> None:
|
|
140
|
+
crosswalk_date = ISO42001_BUNDLE["source_crosswalk_date"]
|
|
141
|
+
console.print(Panel(
|
|
142
|
+
f"Derived from AIUC-1 crosswalk ({crosswalk_date}) + IRIS evidence",
|
|
143
|
+
title=f"ISO 42001 Coverage — {agent_name}",
|
|
144
|
+
style="blue",
|
|
145
|
+
))
|
|
146
|
+
|
|
147
|
+
warning = stale_crosswalk_warning()
|
|
148
|
+
if warning:
|
|
149
|
+
console.print(f"\n[yellow]⚠ {warning}[/yellow]")
|
|
150
|
+
|
|
151
|
+
counts = coverage_breakdown()
|
|
152
|
+
console.print(
|
|
153
|
+
f"\n[green]FULL[/green] coverage (IRIS evidence satisfies the clause):"
|
|
154
|
+
f" {counts['FULL']} clauses"
|
|
155
|
+
)
|
|
156
|
+
console.print(
|
|
157
|
+
f"[yellow]PARTIAL[/yellow] coverage (IRIS evidence + org action needed):"
|
|
158
|
+
f" {counts['PARTIAL']} clauses"
|
|
159
|
+
)
|
|
160
|
+
console.print(
|
|
161
|
+
f"[dim]NOT APPLICABLE[/dim] (organizational/human activity only):"
|
|
162
|
+
f" {counts['NOT_APPLICABLE']} clauses"
|
|
163
|
+
)
|
|
164
|
+
if counts.get("NONE", 0):
|
|
165
|
+
console.print(
|
|
166
|
+
f"[red]NONE[/red] (IRIS inherits AIUC-1 gap — no technical evidence):"
|
|
167
|
+
f" {counts['NONE']} clauses"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
for tier, style in (
|
|
171
|
+
("FULL", "green"),
|
|
172
|
+
("PARTIAL", "yellow"),
|
|
173
|
+
("NOT_APPLICABLE", "dim"),
|
|
174
|
+
("NONE", "red"),
|
|
175
|
+
):
|
|
176
|
+
section = [c for c in ISO42001_CLAUSES if c.get("iris_coverage") == tier]
|
|
177
|
+
if not section:
|
|
178
|
+
continue
|
|
179
|
+
console.print(f"\n[bold {style}]{tier}[/bold {style}] ({len(section)} clauses)")
|
|
180
|
+
table = Table(show_header=True, header_style="bold")
|
|
181
|
+
table.add_column("Clause")
|
|
182
|
+
table.add_column("Title")
|
|
183
|
+
table.add_column("AIUC-1 Gap")
|
|
184
|
+
table.add_column("Evidence / Gap note", overflow="fold")
|
|
185
|
+
for clause in section:
|
|
186
|
+
note = clause.get("iris_evidence") or clause.get("gap_note") or "—"
|
|
187
|
+
table.add_row(
|
|
188
|
+
clause["clause"],
|
|
189
|
+
clause["title"],
|
|
190
|
+
clause["aiuc1_gap"],
|
|
191
|
+
note,
|
|
192
|
+
)
|
|
193
|
+
console.print(table)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _render_aiuc1_certification(agent_name: str, passport: AgentPassport) -> None:
|
|
197
|
+
result = run_framework_check(passport, "aiuc-1")
|
|
198
|
+
bundle = load_bundle_data("aiuc-1")
|
|
199
|
+
console.print(Panel(
|
|
200
|
+
f"Agent: {agent_name}\n"
|
|
201
|
+
f"Framework: {bundle.get('full_name', 'AIUC-1')}\n"
|
|
202
|
+
f"Source crosswalk: {bundle.get('source_crosswalk_date', '2025-09-18')}\n"
|
|
203
|
+
f"Generated: {datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')}",
|
|
204
|
+
title="IRIS Certification — aiuc-1",
|
|
205
|
+
style="blue",
|
|
206
|
+
))
|
|
207
|
+
table = Table(show_header=True, header_style="bold")
|
|
208
|
+
table.add_column("Control")
|
|
209
|
+
table.add_column("Coverage")
|
|
210
|
+
table.add_column("Status")
|
|
211
|
+
table.add_column("Notes", overflow="fold")
|
|
212
|
+
for rule_result in result.rule_results:
|
|
213
|
+
bundle_rule = next(
|
|
214
|
+
(r for r in bundle.get("rules", []) if r["rule_id"] == rule_result.rule_id),
|
|
215
|
+
{},
|
|
216
|
+
)
|
|
217
|
+
coverage = bundle_rule.get("coverage", "")
|
|
218
|
+
if rule_result.status == "OUT_OF_SCOPE":
|
|
219
|
+
status = "[dim]OUT OF SCOPE[/dim]"
|
|
220
|
+
note = bundle_rule.get("gap_note", "")
|
|
221
|
+
elif rule_result.status == "PASS":
|
|
222
|
+
status = "[green]PASS[/green]"
|
|
223
|
+
note = bundle_rule.get("gap_note", "") if coverage == "PARTIAL" else ""
|
|
224
|
+
else:
|
|
225
|
+
status = "[red]FAIL[/red]"
|
|
226
|
+
note = rule_result.message
|
|
227
|
+
table.add_row(rule_result.rule_id, coverage, status, note)
|
|
228
|
+
console.print(table)
|
|
229
|
+
console.print(
|
|
230
|
+
"\n[dim]Export auditor-ready evidence: "
|
|
231
|
+
f"iris certify --framework aiuc-1 --agent {agent_name} "
|
|
232
|
+
"--format aiuc1-export[/dim]"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
123
236
|
@click.command("certify")
|
|
124
237
|
@click.option(
|
|
125
238
|
"--framework",
|
|
126
239
|
required=True,
|
|
127
|
-
type=click.Choice(
|
|
128
|
-
"colorado-ai-act", "nist-ai-rmf", "fedramp-moderate", "hipaa", "soc2", "gdpr",
|
|
129
|
-
"nyc-ll144", "illinois-ai-video", "eu-ai-act", "ccpa-admt", "china-pipl",
|
|
130
|
-
])),
|
|
240
|
+
type=click.Choice(_CERTIFY_FRAMEWORKS),
|
|
131
241
|
)
|
|
132
242
|
@click.option("--agent", "agent_name", required=True, help="Agent name under governance/agents")
|
|
133
|
-
@click.option(
|
|
243
|
+
@click.option(
|
|
244
|
+
"--format",
|
|
245
|
+
"output_format",
|
|
246
|
+
default="table",
|
|
247
|
+
type=click.Choice(["table", "json", "markdown", "aiuc1-export"]),
|
|
248
|
+
)
|
|
134
249
|
@click.option(
|
|
135
250
|
"--registry-version",
|
|
136
251
|
default=None,
|
|
@@ -157,6 +272,51 @@ def certify_cmd(
|
|
|
157
272
|
raise click.ClickException(f"Passport not found: {passport_path}")
|
|
158
273
|
passport = AgentPassport.from_yaml(passport_path.read_text())
|
|
159
274
|
|
|
275
|
+
if framework == "iso-42001":
|
|
276
|
+
Entitlements().require(Feature.BUNDLE_ISO42001, context="iso-42001 certification")
|
|
277
|
+
if output_format == "json":
|
|
278
|
+
click.echo(
|
|
279
|
+
json.dumps(
|
|
280
|
+
{
|
|
281
|
+
"framework": "iso-42001",
|
|
282
|
+
"agent": agent_name,
|
|
283
|
+
"source_crosswalk_date": ISO42001_BUNDLE["source_crosswalk_date"],
|
|
284
|
+
"coverage_breakdown": coverage_breakdown(),
|
|
285
|
+
"clauses": ISO42001_CLAUSES,
|
|
286
|
+
"stale_warning": stale_crosswalk_warning(),
|
|
287
|
+
},
|
|
288
|
+
indent=2,
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
else:
|
|
292
|
+
_render_iso42001_certification(agent_name)
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
if framework == "aiuc-1":
|
|
296
|
+
Entitlements().require(Feature.BUNDLE_AIUC1, context="aiuc-1 certification")
|
|
297
|
+
if output_format == "aiuc1-export":
|
|
298
|
+
exporter = AIUC1EvidenceExporter(passport, gov_dir)
|
|
299
|
+
click.echo(json.dumps(exporter.export_full_package(agent_name), indent=2))
|
|
300
|
+
return
|
|
301
|
+
if output_format in ("json", "markdown"):
|
|
302
|
+
result = run_framework_check(passport, "aiuc-1")
|
|
303
|
+
payload = {
|
|
304
|
+
"framework": "aiuc-1",
|
|
305
|
+
"agent": agent_name,
|
|
306
|
+
"rules": [
|
|
307
|
+
{
|
|
308
|
+
"rule_id": r.rule_id,
|
|
309
|
+
"status": r.status,
|
|
310
|
+
"message": r.message,
|
|
311
|
+
}
|
|
312
|
+
for r in result.rule_results
|
|
313
|
+
],
|
|
314
|
+
}
|
|
315
|
+
click.echo(json.dumps(payload, indent=2) if output_format == "json" else str(payload))
|
|
316
|
+
return
|
|
317
|
+
_render_aiuc1_certification(agent_name, passport)
|
|
318
|
+
return
|
|
319
|
+
|
|
160
320
|
loader = DynamicBundleLoader(pinned_version=registry_version)
|
|
161
321
|
|
|
162
322
|
if not offline and not registry_version:
|
|
@@ -11,7 +11,7 @@ from rich.console import Console
|
|
|
11
11
|
from rich.table import Table
|
|
12
12
|
|
|
13
13
|
from iris import AgentPassport
|
|
14
|
-
from iris_core.compliance.framework_check import run_framework_check
|
|
14
|
+
from iris_core.compliance.framework_check import load_bundle_data, run_framework_check
|
|
15
15
|
from iris_core.compliance.results import ComplianceCheckStatus
|
|
16
16
|
from iris_core.entitlements.display import build_pro_preview_box
|
|
17
17
|
|
|
@@ -61,29 +61,50 @@ def _render_result(
|
|
|
61
61
|
)
|
|
62
62
|
return False
|
|
63
63
|
|
|
64
|
-
has_failures = any(
|
|
65
|
-
result.
|
|
66
|
-
)
|
|
64
|
+
has_failures = any(
|
|
65
|
+
rule.status == "FAIL" for rule in result.rule_results
|
|
66
|
+
) or bool(result.violations)
|
|
67
67
|
status = "[bold green]PASS[/bold green]" if not has_failures else "[bold red]FAIL[/bold red]"
|
|
68
68
|
console.print(f"\nAgent: [cyan]{passport.name}[/cyan] — {status}")
|
|
69
69
|
|
|
70
70
|
if result.rule_results:
|
|
71
|
+
bundle_rules = []
|
|
72
|
+
if framework == "aiuc-1":
|
|
73
|
+
try:
|
|
74
|
+
bundle_rules = load_bundle_data(framework).get("rules", [])
|
|
75
|
+
except ValueError:
|
|
76
|
+
bundle_rules = []
|
|
71
77
|
table = Table(title=f"{result.framework_name}", show_header=True, header_style="bold")
|
|
72
78
|
table.add_column("Rule", style="cyan")
|
|
73
79
|
table.add_column("Severity")
|
|
74
80
|
table.add_column("Status")
|
|
81
|
+
if framework == "aiuc-1":
|
|
82
|
+
table.add_column("Coverage")
|
|
75
83
|
table.add_column("Response")
|
|
76
84
|
table.add_column("Notes")
|
|
77
85
|
for rule in result.rule_results:
|
|
78
|
-
|
|
86
|
+
if rule.status == "PASS":
|
|
87
|
+
status_icon = "[green]✓ PASS[/green]"
|
|
88
|
+
elif rule.status == "OUT_OF_SCOPE":
|
|
89
|
+
status_icon = "[dim]— OUT OF SCOPE[/dim]"
|
|
90
|
+
else:
|
|
91
|
+
status_icon = "[red]✗ FAIL[/red]"
|
|
79
92
|
notes = rule.message if rule.status == "FAIL" else ""
|
|
80
|
-
|
|
81
|
-
rule.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
93
|
+
if rule.status == "OUT_OF_SCOPE":
|
|
94
|
+
notes = rule.message
|
|
95
|
+
row = [rule.rule_id, rule.severity, status_icon]
|
|
96
|
+
if framework == "aiuc-1":
|
|
97
|
+
bundle_rule = next(
|
|
98
|
+
(
|
|
99
|
+
r
|
|
100
|
+
for r in bundle_rules
|
|
101
|
+
if r.get("rule_id") == rule.rule_id
|
|
102
|
+
),
|
|
103
|
+
{},
|
|
104
|
+
)
|
|
105
|
+
row.append(bundle_rule.get("coverage", ""))
|
|
106
|
+
row.extend([rule.response, notes[:80] + ("..." if len(notes) > 80 else "")])
|
|
107
|
+
table.add_row(*row)
|
|
87
108
|
console.print(table)
|
|
88
109
|
console.print(
|
|
89
110
|
"\n[dim]RESPONSE KEY: INFORM — log and proceed | "
|
|
@@ -9,6 +9,8 @@ import click
|
|
|
9
9
|
from rich.console import Console
|
|
10
10
|
from rich.panel import Panel
|
|
11
11
|
|
|
12
|
+
from iris_core.cli_timing.instrument import timed_cli_command
|
|
13
|
+
|
|
12
14
|
console = Console()
|
|
13
15
|
|
|
14
16
|
_DATA_CLASSIFICATIONS = {
|
|
@@ -127,6 +129,7 @@ def _interactive_wizard(output_dir: Optional[Path]) -> None:
|
|
|
127
129
|
@click.option("--high-risk", is_flag=True, default=False, help="Flag as high-risk AI")
|
|
128
130
|
@click.option("--compliance", "-c", multiple=True, help="Compliance frameworks to tag")
|
|
129
131
|
@click.option("--dir", "output_dir", type=click.Path(path_type=Path), default=None)
|
|
132
|
+
@timed_cli_command("iris declare")
|
|
130
133
|
def declare(
|
|
131
134
|
name: Optional[str],
|
|
132
135
|
owner: Optional[str],
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Org-wide discovery CLI — iris discover org."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import List, Optional
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
|
|
13
|
+
from iris_core.discovery.coordinator import DiscoveryCoordinator
|
|
14
|
+
from iris_core.discovery.sources import (
|
|
15
|
+
CICDDiscoverySource,
|
|
16
|
+
KubernetesDiscoverySource,
|
|
17
|
+
MCPRegistryDiscoverySource,
|
|
18
|
+
SCMDiscoverySource,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
SOURCE_MAP = {
|
|
22
|
+
"scm": SCMDiscoverySource,
|
|
23
|
+
"cicd": CICDDiscoverySource,
|
|
24
|
+
"k8s": KubernetesDiscoverySource,
|
|
25
|
+
"mcp": MCPRegistryDiscoverySource,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
FRAMEWORK_LABELS = {
|
|
29
|
+
"langchain": "LangChain",
|
|
30
|
+
"openai_sdk": "OpenAI SDK",
|
|
31
|
+
"anthropic_sdk": "Anthropic SDK",
|
|
32
|
+
"bedrock": "Bedrock",
|
|
33
|
+
"gemini": "Gemini",
|
|
34
|
+
"crewai": "CrewAI",
|
|
35
|
+
"autogen": "Autogen",
|
|
36
|
+
"mcp_server": "MCP Server",
|
|
37
|
+
"custom": "Custom/unknown",
|
|
38
|
+
"unknown": "Custom/unknown",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _format_duration(seconds: float) -> str:
|
|
43
|
+
minutes = int(seconds // 60)
|
|
44
|
+
secs = int(seconds % 60)
|
|
45
|
+
if minutes:
|
|
46
|
+
return f"{minutes}m {secs}s"
|
|
47
|
+
return f"{secs}s"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _resolve_scm_config(org_name: str, config_file: Optional[Path]) -> dict:
|
|
51
|
+
if config_file and config_file.exists():
|
|
52
|
+
data = json.loads(config_file.read_text())
|
|
53
|
+
scm = data.get("scm", data)
|
|
54
|
+
scm.setdefault("org", org_name)
|
|
55
|
+
return scm
|
|
56
|
+
|
|
57
|
+
token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
|
|
58
|
+
config: dict = {"provider": "github", "org": org_name}
|
|
59
|
+
if token:
|
|
60
|
+
config["token"] = token
|
|
61
|
+
if os.environ.get("IRIS_DISCOVERY_REPOS"):
|
|
62
|
+
config["repos"] = json.loads(os.environ["IRIS_DISCOVERY_REPOS"])
|
|
63
|
+
return config
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _build_configs(
|
|
67
|
+
source_names: List[str],
|
|
68
|
+
org_name: str,
|
|
69
|
+
config_file: Optional[Path],
|
|
70
|
+
) -> dict:
|
|
71
|
+
configs: dict = {}
|
|
72
|
+
if config_file and config_file.exists():
|
|
73
|
+
all_cfg = json.loads(config_file.read_text())
|
|
74
|
+
else:
|
|
75
|
+
all_cfg = {}
|
|
76
|
+
|
|
77
|
+
if "scm" in source_names:
|
|
78
|
+
configs["scm_repo"] = all_cfg.get("scm", _resolve_scm_config(org_name, config_file))
|
|
79
|
+
configs["scm_repo"].setdefault("org", org_name)
|
|
80
|
+
|
|
81
|
+
if "cicd" in source_names:
|
|
82
|
+
cicd = all_cfg.get("cicd", {})
|
|
83
|
+
if configs.get("scm_repo", {}).get("repos"):
|
|
84
|
+
cicd["repos"] = configs["scm_repo"]["repos"]
|
|
85
|
+
configs["cicd_pipeline"] = cicd
|
|
86
|
+
|
|
87
|
+
if "k8s" in source_names:
|
|
88
|
+
k8s = all_cfg.get("k8s", {})
|
|
89
|
+
kubeconfig = os.environ.get("KUBECONFIG")
|
|
90
|
+
if kubeconfig:
|
|
91
|
+
k8s.setdefault("kubeconfig", kubeconfig)
|
|
92
|
+
configs["k8s_workload"] = k8s
|
|
93
|
+
|
|
94
|
+
if "mcp" in source_names:
|
|
95
|
+
configs["mcp_registry"] = all_cfg.get("mcp", {})
|
|
96
|
+
|
|
97
|
+
return configs
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def render_org_discovery_report(inventory, duration_seconds: float) -> str:
|
|
101
|
+
"""Render CLI output matching the Brownfield demo format."""
|
|
102
|
+
lines: List[str] = []
|
|
103
|
+
org = inventory.org_id
|
|
104
|
+
|
|
105
|
+
repos = 0
|
|
106
|
+
pipelines = 0
|
|
107
|
+
clusters = 0
|
|
108
|
+
for src in inventory.sources_scanned:
|
|
109
|
+
if src.get("source_type") == "scm_repo":
|
|
110
|
+
repos = src.get("repos_scanned", repos)
|
|
111
|
+
elif src.get("source_type") == "cicd_pipeline":
|
|
112
|
+
pipelines = src.get("findings_count", pipelines)
|
|
113
|
+
elif src.get("source_type") == "k8s_workload":
|
|
114
|
+
clusters = src.get("clusters_scanned", 1 if src.get("status") == "complete" else 0)
|
|
115
|
+
|
|
116
|
+
cicd_count = sum(
|
|
117
|
+
1
|
|
118
|
+
for a in inventory.agents
|
|
119
|
+
if a.source_type == "cicd_pipeline" and a.confidence == "certain"
|
|
120
|
+
)
|
|
121
|
+
if pipelines == 0 and cicd_count:
|
|
122
|
+
pipelines = cicd_count
|
|
123
|
+
|
|
124
|
+
lines.append(f"IRIS Org Discovery — {org}")
|
|
125
|
+
lines.append(
|
|
126
|
+
f"Scanned: {repos} repos, {pipelines} CI/CD pipelines, {clusters} K8s clusters"
|
|
127
|
+
)
|
|
128
|
+
lines.append(
|
|
129
|
+
f"Duration: {_format_duration(duration_seconds)}, Status: {inventory.status}"
|
|
130
|
+
)
|
|
131
|
+
lines.append("")
|
|
132
|
+
lines.append(f"Found {inventory.total_agents} agents across your organization.")
|
|
133
|
+
lines.append(
|
|
134
|
+
f"{inventory.ungoverned_count} are ungoverned — no IRIS policy, no audit trail."
|
|
135
|
+
)
|
|
136
|
+
lines.append("")
|
|
137
|
+
lines.append("By framework:")
|
|
138
|
+
|
|
139
|
+
ungoverned_by_fw = inventory.ungoverned_by_framework()
|
|
140
|
+
fw_counts = inventory.by_framework
|
|
141
|
+
sorted_fw = sorted(
|
|
142
|
+
fw_counts.keys(),
|
|
143
|
+
key=lambda k: fw_counts[k],
|
|
144
|
+
reverse=True,
|
|
145
|
+
)
|
|
146
|
+
for fw in sorted_fw:
|
|
147
|
+
label = FRAMEWORK_LABELS.get(fw, fw.replace("_", " ").title())
|
|
148
|
+
total = fw_counts[fw]
|
|
149
|
+
ungov = ungoverned_by_fw.get(fw, 0)
|
|
150
|
+
lines.append(f" {label:<18} {total:>4} ({ungov} ungoverned)")
|
|
151
|
+
|
|
152
|
+
prod_unowned = inventory.ungoverned_production_without_owner()
|
|
153
|
+
if prod_unowned:
|
|
154
|
+
lines.append("")
|
|
155
|
+
lines.append(
|
|
156
|
+
f"{prod_unowned} ungoverned agents are in PRODUCTION with no owner on record."
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
lines.append("Run: iris discover org --report for the full inventory.")
|
|
160
|
+
return "\n".join(lines)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@click.group()
|
|
164
|
+
def discover():
|
|
165
|
+
"""Org-wide AI agent discovery across SCM, CI/CD, Kubernetes, and MCP."""
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@discover.command("org")
|
|
170
|
+
@click.option(
|
|
171
|
+
"--sources",
|
|
172
|
+
default="scm,cicd,k8s,mcp",
|
|
173
|
+
show_default=True,
|
|
174
|
+
help="Comma-separated discovery sources",
|
|
175
|
+
)
|
|
176
|
+
@click.option("--org-name", required=True, help="Organization identifier for this scan")
|
|
177
|
+
@click.option(
|
|
178
|
+
"--config",
|
|
179
|
+
"config_file",
|
|
180
|
+
type=click.Path(exists=False, path_type=Path),
|
|
181
|
+
default=None,
|
|
182
|
+
help="JSON config file with source credentials and repo fixtures",
|
|
183
|
+
)
|
|
184
|
+
@click.option("--report", is_flag=True, help="Output full JSON inventory")
|
|
185
|
+
@click.option(
|
|
186
|
+
"--vault-dir",
|
|
187
|
+
type=click.Path(path_type=Path),
|
|
188
|
+
default=None,
|
|
189
|
+
help="Evidence vault directory for scan results",
|
|
190
|
+
)
|
|
191
|
+
def discover_org(sources: str, org_name: str, config_file: Optional[Path], report: bool, vault_dir: Optional[Path]):
|
|
192
|
+
"""
|
|
193
|
+
Scan your organization for AI agents across all configured surfaces.
|
|
194
|
+
|
|
195
|
+
Example:
|
|
196
|
+
iris discover org --sources scm --org-name acme-corp
|
|
197
|
+
"""
|
|
198
|
+
source_names = [s.strip() for s in sources.split(",") if s.strip()]
|
|
199
|
+
unknown = set(source_names) - set(SOURCE_MAP)
|
|
200
|
+
if unknown:
|
|
201
|
+
raise click.ClickException(f"Unknown sources: {', '.join(sorted(unknown))}")
|
|
202
|
+
|
|
203
|
+
if not config_file and not os.environ.get("GITHUB_TOKEN") and "scm" in source_names:
|
|
204
|
+
if not os.environ.get("IRIS_DISCOVERY_REPOS"):
|
|
205
|
+
token = click.prompt("GitHub token (contents:read)", hide_input=True, default="", show_default=False)
|
|
206
|
+
if token:
|
|
207
|
+
os.environ["GITHUB_TOKEN"] = token
|
|
208
|
+
org_name = click.prompt("GitHub organization name", default=org_name)
|
|
209
|
+
|
|
210
|
+
configs = _build_configs(source_names, org_name, config_file)
|
|
211
|
+
instances = [SOURCE_MAP[name]() for name in source_names]
|
|
212
|
+
coordinator = DiscoveryCoordinator(vault_dir=vault_dir)
|
|
213
|
+
|
|
214
|
+
start = time.monotonic()
|
|
215
|
+
inventory = coordinator.run_scan_sync(org_name, instances, configs)
|
|
216
|
+
duration = time.monotonic() - start
|
|
217
|
+
|
|
218
|
+
if report:
|
|
219
|
+
payload = inventory.to_dict()
|
|
220
|
+
payload["duration_seconds"] = duration
|
|
221
|
+
click.echo(json.dumps(payload, indent=2))
|
|
222
|
+
else:
|
|
223
|
+
click.echo(render_org_discovery_report(inventory, duration))
|