iris-security-cli 0.2.4__tar.gz → 0.2.11__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.4 → iris_security_cli-0.2.11}/PKG-INFO +3 -3
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/certify.py +165 -5
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/compliance_check_cmd.py +33 -12
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/declare.py +3 -0
- iris_security_cli-0.2.11/iris_cli/discover.py +223 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/evidence.py +66 -1
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/framework_suggest.py +36 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/license_cmd.py +12 -6
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/main.py +13 -1
- iris_security_cli-0.2.11/iris_cli/onboarding_report.py +46 -0
- iris_security_cli-0.2.11/iris_cli/org_policy.py +399 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/quickstart.py +2 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_security_cli.egg-info/PKG-INFO +3 -3
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_security_cli.egg-info/SOURCES.txt +3 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_security_cli.egg-info/requires.txt +2 -2
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/pyproject.toml +3 -3
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/README.md +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/__init__.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/action_plan.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/assess.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/cedar_parser.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/compiler_config.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/compliance_fix.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/cost.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/delegation.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/dlp_cmd.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/drift.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/enforce.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/entitlements_cmd.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/explain.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/framework_test.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/hitl.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/mcp_server.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/models_cmd.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/policy_cache.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/policy_diff.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/preview.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/redteam.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/regulatory.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/scan_govern.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/scan_report.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/scm.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/sentinel.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/status_cmd.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/users.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/vault.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/watch.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_cli/witness.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_security_cli.egg-info/dependency_links.txt +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_security_cli.egg-info/entry_points.txt +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/iris_security_cli.egg-info/top_level.txt +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/setup.cfg +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/tests/test_entitlements_display.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/tests/test_evidence.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/tests/test_framework_suggest.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/tests/test_framework_test.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/tests/test_hitl_cli.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/tests/test_mcp_model_governance.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/tests/test_policy_diff.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/tests/test_quickstart.py +0 -0
- {iris_security_cli-0.2.4 → iris_security_cli-0.2.11}/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.11
|
|
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.11
|
|
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))
|
|
@@ -21,6 +21,7 @@ from rich.table import Table
|
|
|
21
21
|
|
|
22
22
|
from iris_core.compliance.bundles.colorado_ai_act import get_colorado_rules
|
|
23
23
|
from iris_core.evidence.vault import EvidenceVault, VaultSummary
|
|
24
|
+
from iris_core.evidence.vault_v2 import EvidenceVaultV2
|
|
24
25
|
from iris_core.models.passport import AgentPassport
|
|
25
26
|
|
|
26
27
|
console = Console()
|
|
@@ -719,10 +720,60 @@ def evidence_list(agent, violations_only, limit, governance_dir, vault_dir):
|
|
|
719
720
|
console.print(_events_table(events))
|
|
720
721
|
|
|
721
722
|
|
|
723
|
+
@evidence.command("record-cicd")
|
|
724
|
+
@click.option(
|
|
725
|
+
"--system",
|
|
726
|
+
required=True,
|
|
727
|
+
type=click.Choice(
|
|
728
|
+
["github_actions", "gitlab", "jenkins", "terraform", "argocd"]
|
|
729
|
+
),
|
|
730
|
+
)
|
|
731
|
+
@click.option("--run-id", required=True, help="Pipeline or build run identifier")
|
|
732
|
+
@click.option("--pipeline-url", default="", help="URL to the pipeline run")
|
|
733
|
+
@click.option("--triggered-by", default="automated", help="User or service that triggered the run")
|
|
734
|
+
@click.option("--outcome", default="success", help="Run outcome (success, failure, synced, apply, etc.)")
|
|
735
|
+
@click.option("--agent", default=None, help="Agent ID for vault scoping (defaults to IRIS_AGENT_ID or cwd agent)")
|
|
736
|
+
@click.option("--vault-dir", type=Path, default=None)
|
|
737
|
+
def evidence_record_cicd(system, run_id, pipeline_url, triggered_by, outcome, agent, vault_dir):
|
|
738
|
+
"""
|
|
739
|
+
Record a CI/CD pipeline run as an immutable EvidenceEvent.
|
|
740
|
+
|
|
741
|
+
Example:
|
|
742
|
+
iris evidence record-cicd --system github_actions --run-id test-123 --outcome success
|
|
743
|
+
"""
|
|
744
|
+
import os
|
|
745
|
+
|
|
746
|
+
agent_id = agent or os.environ.get("IRIS_AGENT_ID") or "platform-governance"
|
|
747
|
+
vault = EvidenceVaultV2(agent_id=agent_id, vault_dir=vault_dir)
|
|
748
|
+
event = vault.record_cicd(
|
|
749
|
+
system=system,
|
|
750
|
+
run_id=run_id,
|
|
751
|
+
pipeline_url=pipeline_url,
|
|
752
|
+
triggered_by=triggered_by,
|
|
753
|
+
outcome=outcome,
|
|
754
|
+
)
|
|
755
|
+
click.echo(
|
|
756
|
+
json.dumps(
|
|
757
|
+
{
|
|
758
|
+
"event_id": event.event_id,
|
|
759
|
+
"sequence_number": event.sequence_number,
|
|
760
|
+
"signature": event.signature,
|
|
761
|
+
"timestamp": event.timestamp,
|
|
762
|
+
},
|
|
763
|
+
indent=2,
|
|
764
|
+
)
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
|
|
722
768
|
@evidence.command("export")
|
|
723
769
|
@click.option("--agent", required=True, help="Agent name")
|
|
724
770
|
@click.option("--output", "output_path", required=True, type=Path)
|
|
725
|
-
@click.option(
|
|
771
|
+
@click.option(
|
|
772
|
+
"--format",
|
|
773
|
+
"output_format",
|
|
774
|
+
default="json",
|
|
775
|
+
type=click.Choice(["json", "csv", "aiuc1", "pdf", "oscal"]),
|
|
776
|
+
)
|
|
726
777
|
@click.option("--dir", "governance_dir", type=Path, default=None)
|
|
727
778
|
@click.option("--vault-dir", type=Path, default=None)
|
|
728
779
|
def evidence_export(agent, output_path, output_format, governance_dir, vault_dir):
|
|
@@ -739,6 +790,20 @@ def evidence_export(agent, output_path, output_format, governance_dir, vault_dir
|
|
|
739
790
|
console.print(f"[red]{exc}[/red]")
|
|
740
791
|
raise SystemExit(1)
|
|
741
792
|
|
|
793
|
+
if output_format in {"aiuc1", "pdf", "oscal"}:
|
|
794
|
+
vault_v2 = EvidenceVaultV2(
|
|
795
|
+
agent_id=passport.evidence_vault_id or agent,
|
|
796
|
+
vault_dir=vault_dir,
|
|
797
|
+
)
|
|
798
|
+
response = vault_v2.api.get_export(
|
|
799
|
+
body={},
|
|
800
|
+
query={"agent_id": agent, "format": output_format},
|
|
801
|
+
)
|
|
802
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
803
|
+
output_path.write_text(json.dumps(response.body, indent=2))
|
|
804
|
+
console.print(f"[green]✓ Exported {output_format} evidence to {output_path}[/green]")
|
|
805
|
+
return
|
|
806
|
+
|
|
742
807
|
vault = _open_vault(agent, vault_dir)
|
|
743
808
|
vault_data = vault.export_vault()
|
|
744
809
|
vault_data["passport"] = {
|
|
@@ -63,6 +63,18 @@ VIDEO_KEYWORDS = (
|
|
|
63
63
|
"biometric",
|
|
64
64
|
)
|
|
65
65
|
|
|
66
|
+
HIGH_RISK_CUSTOMER_KEYWORDS = (
|
|
67
|
+
"customer service",
|
|
68
|
+
"customer support",
|
|
69
|
+
"candidate",
|
|
70
|
+
"scoring",
|
|
71
|
+
"interviewer",
|
|
72
|
+
"hiring",
|
|
73
|
+
"recruiting",
|
|
74
|
+
"loan",
|
|
75
|
+
"underwriting",
|
|
76
|
+
)
|
|
77
|
+
|
|
66
78
|
Q1_CHOICES = [
|
|
67
79
|
"Makes decisions that affect individual people (hiring, loans, medical, etc.)",
|
|
68
80
|
"Automates business processes (summarization, classification, routing)",
|
|
@@ -327,6 +339,12 @@ def build_recommendations(answers: dict[str, Any]) -> list[Recommendation]:
|
|
|
327
339
|
or "Other / multiple states" in q8
|
|
328
340
|
)
|
|
329
341
|
)
|
|
342
|
+
aiuc1 = (
|
|
343
|
+
q5
|
|
344
|
+
or q1 == Q1_CHOICES[0]
|
|
345
|
+
or any(kw in agent_description for kw in HIGH_RISK_CUSTOMER_KEYWORDS)
|
|
346
|
+
or any(kw in q1.lower() for kw in HIGH_RISK_CUSTOMER_KEYWORDS)
|
|
347
|
+
)
|
|
330
348
|
|
|
331
349
|
colorado_reason = (
|
|
332
350
|
"Your agent makes consequential decisions for Colorado users. "
|
|
@@ -495,6 +513,24 @@ def build_recommendations(answers: dict[str, Any]) -> list[Recommendation]:
|
|
|
495
513
|
),
|
|
496
514
|
command="iris compliance check --framework nyc-ll144" if nyc_ll144 else None,
|
|
497
515
|
),
|
|
516
|
+
Recommendation(
|
|
517
|
+
framework="aiuc-1",
|
|
518
|
+
tier="PRO",
|
|
519
|
+
status="RECOMMENDED" if aiuc1 else "NOT APPLICABLE",
|
|
520
|
+
reason=(
|
|
521
|
+
"AIUC-1 is a voluntary enterprise-trust certification for "
|
|
522
|
+
"customer-facing and high-risk AI agents (customer service, "
|
|
523
|
+
"candidate scoring, interviewer agents). IRIS exports technical "
|
|
524
|
+
"evidence in AIUC-1's B006-format for accredited auditors."
|
|
525
|
+
if aiuc1
|
|
526
|
+
else "No high-risk customer-facing agent scope detected."
|
|
527
|
+
),
|
|
528
|
+
command=(
|
|
529
|
+
f"iris certify --framework aiuc-1 --agent {answers.get('agent_name', '<agent>')}"
|
|
530
|
+
if aiuc1
|
|
531
|
+
else None
|
|
532
|
+
),
|
|
533
|
+
),
|
|
498
534
|
]
|
|
499
535
|
return recommendations
|
|
500
536
|
|