agenteval-toolkit 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.
- agenteval/__init__.py +5 -0
- agenteval/_starter/certification/policy.yaml +15 -0
- agenteval/_starter/certification/systems/northwind-cs-staging.yaml +16 -0
- agenteval/_starter/certification/systems/northwind-cs.yaml +17 -0
- agenteval/_starter/examples/agents/demo-good.yaml +41 -0
- agenteval/_starter/examples/agents/demo-weak.yaml +34 -0
- agenteval/_starter/examples/agents/openai-compatible.example.yaml +15 -0
- agenteval/_starter/examples/agents/webhook.example.yaml +21 -0
- agenteval/_starter/examples/attestations-certification.yaml +16 -0
- agenteval/_starter/examples/attestations.yaml +19 -0
- agenteval/_starter/examples/snapshots/sample-cluster.json +323 -0
- agenteval/_starter/exams/customer-service/cs-ecommerce-v1.yaml +222 -0
- agenteval/_starter/rules/industry-pci-dss/i18n/zh.yaml +27 -0
- agenteval/_starter/rules/industry-pci-dss/pack.yaml +9 -0
- agenteval/_starter/rules/industry-pci-dss/rules.yaml +83 -0
- agenteval/_starter/rules/us-ca/i18n/zh.yaml +21 -0
- agenteval/_starter/rules/us-ca/pack.yaml +9 -0
- agenteval/_starter/rules/us-ca/rules.yaml +72 -0
- agenteval/_starter/rules/us-federal/governance.yaml +216 -0
- agenteval/_starter/rules/us-federal/i18n/zh.yaml +107 -0
- agenteval/_starter/rules/us-federal/pack.yaml +8 -0
- agenteval/_starter/rules/us-federal/security.yaml +164 -0
- agenteval/certification/__init__.py +16 -0
- agenteval/certification/assess.py +137 -0
- agenteval/certification/issuer.py +165 -0
- agenteval/certification/models.py +113 -0
- agenteval/certification/render.py +100 -0
- agenteval/certification/signing.py +86 -0
- agenteval/cli.py +533 -0
- agenteval/collectors/__init__.py +30 -0
- agenteval/collectors/kubernetes.py +70 -0
- agenteval/collectors/normalize.py +247 -0
- agenteval/config.py +49 -0
- agenteval/engine/__init__.py +12 -0
- agenteval/engine/conditions.py +109 -0
- agenteval/engine/evaluator.py +133 -0
- agenteval/engine/loader.py +131 -0
- agenteval/evaluation/__init__.py +17 -0
- agenteval/evaluation/adapters.py +276 -0
- agenteval/evaluation/assertions.py +73 -0
- agenteval/evaluation/judges.py +214 -0
- agenteval/evaluation/messages.py +56 -0
- agenteval/evaluation/models.py +174 -0
- agenteval/evaluation/papers.py +103 -0
- agenteval/evaluation/report.py +80 -0
- agenteval/evaluation/runner.py +154 -0
- agenteval/i18n.py +118 -0
- agenteval/models.py +136 -0
- agenteval/reports/__init__.py +86 -0
- agenteval/reports/templates/certificate.html.j2 +178 -0
- agenteval/reports/templates/eval_report.html.j2 +191 -0
- agenteval/reports/templates/report.html.j2 +75 -0
- agenteval/schema.py +66 -0
- agenteval/schemas/attestations.schema.json +76 -0
- agenteval/schemas/exam.schema.json +85 -0
- agenteval/schemas/pack.schema.json +48 -0
- agenteval/schemas/policy.schema.json +67 -0
- agenteval/schemas/rule.schema.json +298 -0
- agenteval/schemas/system.schema.json +76 -0
- agenteval/schemas/translations.schema.json +32 -0
- agenteval_toolkit-0.1.0.dist-info/METADATA +307 -0
- agenteval_toolkit-0.1.0.dist-info/RECORD +67 -0
- agenteval_toolkit-0.1.0.dist-info/WHEEL +5 -0
- agenteval_toolkit-0.1.0.dist-info/entry_points.txt +2 -0
- agenteval_toolkit-0.1.0.dist-info/licenses/LICENSE +202 -0
- agenteval_toolkit-0.1.0.dist-info/licenses/NOTICE +19 -0
- agenteval_toolkit-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# yaml-language-server: $schema=../../../agenteval/schemas/translations.schema.json
|
|
2
|
+
# Simplified Chinese translations for us-federal rules.
|
|
3
|
+
# Keys must match rule IDs. `sources` translates each source's `reference` in order.
|
|
4
|
+
FED-INV-001:
|
|
5
|
+
title: AI 系统须登记责任人与模型信息
|
|
6
|
+
description: 每个推理服务须标注责任团队与所服务的模型,以支持 AI 系统清单管理。
|
|
7
|
+
remediation: 为工作负载添加标签 agenteval.io/owner=<team> 与注解 agenteval.io/model=<model-name>。
|
|
8
|
+
sources:
|
|
9
|
+
- GOVERN 1.6(AI 系统清单机制)
|
|
10
|
+
- AI 使用案例清单(适用于联邦机构)
|
|
11
|
+
FED-PRV-001:
|
|
12
|
+
title: 模型须有可追溯的来源并完成完整性校验
|
|
13
|
+
description: 记录模型权重来源(内部训练 / 第三方仓库)并校验哈希或签名。
|
|
14
|
+
remediation: 添加注解 agenteval.io/model-source=<来源>;在拉取权重时校验 sha256 或签名, 通过后设置
|
|
15
|
+
agenteval.io/provenance-verified="true"(建议由 CI/准入控制器自动写入)。
|
|
16
|
+
sources:
|
|
17
|
+
- GOVERN 6.1(第三方 AI 风险策略)
|
|
18
|
+
- 模型与训练数据的完整性保护
|
|
19
|
+
FED-TRN-001:
|
|
20
|
+
title: 模型须附带模型卡或等效文档
|
|
21
|
+
description: 文档应说明模型用途、能力边界、已知局限与评估结果。
|
|
22
|
+
remediation: 添加注解 agenteval.io/model-card=<文档链接>,内容覆盖预期用途、局限性与评估方法。
|
|
23
|
+
sources:
|
|
24
|
+
- MAP 功能(系统能力与局限的记录)
|
|
25
|
+
FED-MON-001:
|
|
26
|
+
title: 推理服务须启用请求日志与运行监控
|
|
27
|
+
description: 部署后需持续监测,以发现滥用、性能退化与安全事件。
|
|
28
|
+
remediation: 启用网关/服务的请求日志(注意脱敏),暴露 Prometheus 指标并添加注解
|
|
29
|
+
agenteval.io/request-logging="true"、prometheus.io/scrape="true"。
|
|
30
|
+
sources:
|
|
31
|
+
- MANAGE 4.1(部署后监测计划)
|
|
32
|
+
FED-SAF-001:
|
|
33
|
+
title: 生成式模型须启用内容安全防护
|
|
34
|
+
description: 生成式模型的输入/输出应经过安全过滤或护栏(如 Llama Guard、NeMo Guardrails)。
|
|
35
|
+
remediation: 部署护栏服务或 sidecar,并添加注解 agenteval.io/content-safety=<方案名>。
|
|
36
|
+
sources:
|
|
37
|
+
- Generative AI Profile(有害内容、信息安全风险)
|
|
38
|
+
FED-GOV-A01:
|
|
39
|
+
title: 组织须具备 AI 治理政策并指定责任人
|
|
40
|
+
description: 书面 AI 风险管理政策,明确角色职责与审批流程,每年复审。
|
|
41
|
+
remediation: 制定并发布 AI 治理政策,指定负责人,通过 attestations 文件提交链接与审批记录。
|
|
42
|
+
sources:
|
|
43
|
+
- GOVERN 1 / GOVERN 2(政策与问责结构)
|
|
44
|
+
FED-IR-001:
|
|
45
|
+
title: 须具备 AI 安全事件响应预案
|
|
46
|
+
description: 覆盖模型滥用、数据泄露、有害输出等场景的响应与上报流程,每年演练或复审。
|
|
47
|
+
remediation: 编写 AI 事件响应预案并纳入现有安全事件流程,提交文档链接。
|
|
48
|
+
sources:
|
|
49
|
+
- MANAGE 4(风险处置与事件沟通)
|
|
50
|
+
FED-GOV-A02:
|
|
51
|
+
title: 高影响 AI 须完成影响评估与部署前测试
|
|
52
|
+
description: 标记为高影响(high-impact)的 AI 服务,上线前须完成 AI 影响评估与实测验证,每年更新。
|
|
53
|
+
remediation: 完成 AI 影响评估与部署前测试报告,经审批后提交至 attestations 文件。
|
|
54
|
+
sources:
|
|
55
|
+
- 高影响 AI 的最低风险管理实践(适用于联邦机构,私营企业可参照)
|
|
56
|
+
- MAP 5.1 / MEASURE 功能
|
|
57
|
+
FED-GOV-A03:
|
|
58
|
+
title: 高影响 AI 须具备人工监督与申诉机制
|
|
59
|
+
description: 对高影响决策提供人工复核、干预与申诉渠道。
|
|
60
|
+
remediation: 制定人工监督与申诉流程文档,并在服务中实现人工复核入口。
|
|
61
|
+
sources:
|
|
62
|
+
- 高影响 AI 的人工监督、干预与问责要求
|
|
63
|
+
FED-SEC-001:
|
|
64
|
+
title: 推理端点必须启用身份认证
|
|
65
|
+
description: 模型推理 API 不得匿名访问,须通过 API Key、OIDC 或网关认证保护。
|
|
66
|
+
remediation: 为推理服务配置认证:vLLM 使用 --api-key 并从 Secret 注入;或在前方部署 oauth2-proxy / Envoy ext_authz / API 网关,并添加注解
|
|
67
|
+
agenteval.io/auth=<method>。
|
|
68
|
+
sources:
|
|
69
|
+
- MEASURE 2.7(AI 系统安全性与韧性评估)
|
|
70
|
+
- AC-3 Access Enforcement
|
|
71
|
+
FED-SEC-002:
|
|
72
|
+
title: 对外暴露的推理端点必须启用 TLS
|
|
73
|
+
description: 通过 LoadBalancer、NodePort 或 Ingress 对外暴露的推理服务,传输必须加密。
|
|
74
|
+
remediation: 在 Ingress 上配置 tls 段并使用受信任证书(如 cert-manager),或在服务网格中启用 mTLS。
|
|
75
|
+
sources:
|
|
76
|
+
- SC-8 Transmission Confidentiality and Integrity
|
|
77
|
+
FED-SEC-003:
|
|
78
|
+
title: 推理服务须受 NetworkPolicy 限制
|
|
79
|
+
description: 推理工作负载所在命名空间应存在选中该 Pod 的 NetworkPolicy,限制横向访问。
|
|
80
|
+
remediation: 为推理 Pod 创建默认拒绝的 NetworkPolicy,仅放行网关/调用方命名空间的入站流量。
|
|
81
|
+
sources:
|
|
82
|
+
- 4.4 Orchestrator Risks(网络隔离)
|
|
83
|
+
FED-SEC-004:
|
|
84
|
+
title: 容器镜像须以 digest 锁定且不使用 latest 标签
|
|
85
|
+
description: 推理镜像应使用 @sha256 摘要引用,保证部署的可追溯性与完整性。
|
|
86
|
+
remediation: 将镜像引用改为 registry/repo@sha256:<digest>,并在 CI 中签名(如 cosign)后再部署。
|
|
87
|
+
sources:
|
|
88
|
+
- SSDF for Generative AI and Dual-Use Foundation Models(软件完整性)
|
|
89
|
+
FED-SEC-005:
|
|
90
|
+
title: 推理容器不得以 root 或特权模式运行
|
|
91
|
+
description: 容器须设置 runAsNonRoot,禁止 privileged 与权限提升。
|
|
92
|
+
remediation: '在 securityContext 中设置 runAsNonRoot: true、privileged: false、allowPrivilegeEscalation: false。 GPU
|
|
93
|
+
访问应通过 device plugin 实现,而非特权容器。'
|
|
94
|
+
sources:
|
|
95
|
+
- 4.1 Image / 4.5 Host OS Risks(最小权限)
|
|
96
|
+
FED-SEC-006:
|
|
97
|
+
title: 不得在环境变量中明文存放密钥
|
|
98
|
+
description: 模型 API Key、HF Token、云凭证等不得以明文 value 写入 Pod 环境变量。
|
|
99
|
+
remediation: 改用 valueFrom.secretKeyRef 或外部密钥管理(Vault / CSI Secrets Store),并轮换已暴露的凭证。
|
|
100
|
+
sources:
|
|
101
|
+
- IA-5 Authenticator Management
|
|
102
|
+
FED-SEC-007:
|
|
103
|
+
title: 推理容器须设置资源限制
|
|
104
|
+
description: 设置 CPU/内存/GPU limits,降低资源耗尽型攻击与失控负载的影响。
|
|
105
|
+
remediation: 为每个容器配置 resources.limits,并在命名空间上设置 ResourceQuota / LimitRange。
|
|
106
|
+
sources:
|
|
107
|
+
- MEASURE 2.7(AI 系统安全性与韧性评估)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# yaml-language-server: $schema=../../agenteval/schemas/pack.schema.json
|
|
2
|
+
id: us-federal
|
|
3
|
+
name: US federal guidance
|
|
4
|
+
name_zh: 美国联邦指引
|
|
5
|
+
tier: national
|
|
6
|
+
jurisdiction: US
|
|
7
|
+
description: NIST AI RMF, NIST AI 600-1, NIST SP 800-53/190/218A, OMB M-25-21
|
|
8
|
+
description_zh: NIST AI RMF、NIST AI 600-1、NIST SP 800-53/190/218A、OMB M-25-21
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# yaml-language-server: $schema=../../agenteval/schemas/rule.schema.json
|
|
2
|
+
# Baseline security controls for inference services (automated checks)
|
|
3
|
+
# Note: verified=false in sources means the mapping has not yet been reviewed by legal/compliance.
|
|
4
|
+
rules:
|
|
5
|
+
- id: FED-SEC-001
|
|
6
|
+
title: Inference endpoints must require authentication
|
|
7
|
+
description: >-
|
|
8
|
+
Model inference APIs must not allow anonymous access; protect them with an API key, OIDC, or gateway authentication.
|
|
9
|
+
severity: high
|
|
10
|
+
type: automated
|
|
11
|
+
category: access-control
|
|
12
|
+
applies_to:
|
|
13
|
+
asset_types: [inference_service]
|
|
14
|
+
check:
|
|
15
|
+
field: auth.enabled
|
|
16
|
+
op: eq
|
|
17
|
+
value: true
|
|
18
|
+
remediation: >-
|
|
19
|
+
Configure authentication for the inference service: for vLLM use --api-key injected from a Secret; or front
|
|
20
|
+
it with oauth2-proxy / Envoy ext_authz / an API gateway, and add the annotation agenteval.io/auth=<method>.
|
|
21
|
+
sources:
|
|
22
|
+
- framework: NIST AI RMF 1.0
|
|
23
|
+
reference: MEASURE 2.7 (AI system security and resilience evaluated)
|
|
24
|
+
url: https://www.nist.gov/itl/ai-risk-management-framework
|
|
25
|
+
- framework: NIST SP 800-53 Rev.5
|
|
26
|
+
reference: AC-3 Access Enforcement
|
|
27
|
+
url: https://doi.org/10.6028/NIST.SP.800-53r5
|
|
28
|
+
|
|
29
|
+
- id: FED-SEC-002
|
|
30
|
+
title: Externally exposed inference endpoints must use TLS
|
|
31
|
+
description: Inference services exposed via LoadBalancer, NodePort, or Ingress must encrypt traffic in
|
|
32
|
+
transit.
|
|
33
|
+
severity: high
|
|
34
|
+
type: automated
|
|
35
|
+
category: data-protection
|
|
36
|
+
applies_to:
|
|
37
|
+
asset_types: [inference_service]
|
|
38
|
+
when:
|
|
39
|
+
field: exposure.public
|
|
40
|
+
op: eq
|
|
41
|
+
value: true
|
|
42
|
+
check:
|
|
43
|
+
field: tls.enabled
|
|
44
|
+
op: eq
|
|
45
|
+
value: true
|
|
46
|
+
remediation: >-
|
|
47
|
+
Configure a tls section on the Ingress with a trusted certificate (e.g., cert-manager), or enable mTLS in
|
|
48
|
+
the service mesh.
|
|
49
|
+
sources:
|
|
50
|
+
- framework: NIST SP 800-53 Rev.5
|
|
51
|
+
reference: SC-8 Transmission Confidentiality and Integrity
|
|
52
|
+
url: https://doi.org/10.6028/NIST.SP.800-53r5
|
|
53
|
+
|
|
54
|
+
- id: FED-SEC-003
|
|
55
|
+
title: Inference services must be restricted by a NetworkPolicy
|
|
56
|
+
description: >-
|
|
57
|
+
The namespace of an inference workload should have a NetworkPolicy selecting its Pods to limit lateral access.
|
|
58
|
+
severity: medium
|
|
59
|
+
type: automated
|
|
60
|
+
category: network
|
|
61
|
+
applies_to:
|
|
62
|
+
asset_types: [inference_service]
|
|
63
|
+
check:
|
|
64
|
+
field: network_policy.present
|
|
65
|
+
op: eq
|
|
66
|
+
value: true
|
|
67
|
+
remediation: >-
|
|
68
|
+
Create a default-deny NetworkPolicy for inference Pods, allowing ingress only from gateway/caller namespaces.
|
|
69
|
+
sources:
|
|
70
|
+
- framework: NIST SP 800-190
|
|
71
|
+
reference: 4.4 Orchestrator Risks (network segmentation)
|
|
72
|
+
url: https://doi.org/10.6028/NIST.SP.800-190
|
|
73
|
+
|
|
74
|
+
- id: FED-SEC-004
|
|
75
|
+
title: Container images must be pinned by digest and must not use the latest tag
|
|
76
|
+
description: >-
|
|
77
|
+
Inference images should be referenced by @sha256 digest to ensure deployment traceability and integrity.
|
|
78
|
+
severity: medium
|
|
79
|
+
type: automated
|
|
80
|
+
category: supply-chain
|
|
81
|
+
applies_to:
|
|
82
|
+
asset_types: [inference_service]
|
|
83
|
+
check:
|
|
84
|
+
all:
|
|
85
|
+
- field: image.all_digest_pinned
|
|
86
|
+
op: eq
|
|
87
|
+
value: true
|
|
88
|
+
- field: image.any_latest
|
|
89
|
+
op: eq
|
|
90
|
+
value: false
|
|
91
|
+
remediation: >-
|
|
92
|
+
Change image references to registry/repo@sha256:<digest>, and sign images in CI (e.g., cosign) before deployment.
|
|
93
|
+
sources:
|
|
94
|
+
- framework: NIST SP 800-218A
|
|
95
|
+
reference: SSDF for Generative AI and Dual-Use Foundation Models (software integrity)
|
|
96
|
+
url: https://doi.org/10.6028/NIST.SP.800-218A
|
|
97
|
+
|
|
98
|
+
- id: FED-SEC-005
|
|
99
|
+
title: Inference containers must not run as root or in privileged mode
|
|
100
|
+
description: Containers must set runAsNonRoot and disallow privileged mode and privilege escalation.
|
|
101
|
+
severity: high
|
|
102
|
+
type: automated
|
|
103
|
+
category: workload-hardening
|
|
104
|
+
applies_to:
|
|
105
|
+
asset_types: [inference_service]
|
|
106
|
+
check:
|
|
107
|
+
all:
|
|
108
|
+
- field: security_context.run_as_non_root
|
|
109
|
+
op: eq
|
|
110
|
+
value: true
|
|
111
|
+
- field: security_context.privileged
|
|
112
|
+
op: eq
|
|
113
|
+
value: false
|
|
114
|
+
- field: security_context.allow_privilege_escalation
|
|
115
|
+
op: eq
|
|
116
|
+
value: false
|
|
117
|
+
remediation: >-
|
|
118
|
+
Set runAsNonRoot: true, privileged: false, and allowPrivilegeEscalation: false in securityContext. Provide
|
|
119
|
+
GPU access through a device plugin rather than privileged containers.
|
|
120
|
+
sources:
|
|
121
|
+
- framework: NIST SP 800-190
|
|
122
|
+
reference: 4.1 Image / 4.5 Host OS Risks (least privilege)
|
|
123
|
+
url: https://doi.org/10.6028/NIST.SP.800-190
|
|
124
|
+
|
|
125
|
+
- id: FED-SEC-006
|
|
126
|
+
title: Secrets must not be stored in plaintext environment variables
|
|
127
|
+
description: >-
|
|
128
|
+
Model API keys, HF tokens, cloud credentials, etc. must not be written as plaintext values in Pod environment
|
|
129
|
+
variables.
|
|
130
|
+
severity: critical
|
|
131
|
+
type: automated
|
|
132
|
+
category: secrets
|
|
133
|
+
applies_to:
|
|
134
|
+
asset_types: [inference_service]
|
|
135
|
+
check:
|
|
136
|
+
field: secrets.plaintext_env
|
|
137
|
+
op: empty
|
|
138
|
+
remediation: >-
|
|
139
|
+
Use valueFrom.secretKeyRef or an external secrets manager (Vault / CSI Secrets Store), and rotate any exposed
|
|
140
|
+
credentials.
|
|
141
|
+
sources:
|
|
142
|
+
- framework: NIST SP 800-53 Rev.5
|
|
143
|
+
reference: IA-5 Authenticator Management
|
|
144
|
+
url: https://doi.org/10.6028/NIST.SP.800-53r5
|
|
145
|
+
|
|
146
|
+
- id: FED-SEC-007
|
|
147
|
+
title: Inference containers must set resource limits
|
|
148
|
+
description: Set CPU/memory/GPU limits to reduce the impact of resource-exhaustion attacks and runaway
|
|
149
|
+
workloads.
|
|
150
|
+
severity: low
|
|
151
|
+
type: automated
|
|
152
|
+
category: resilience
|
|
153
|
+
applies_to:
|
|
154
|
+
asset_types: [inference_service]
|
|
155
|
+
check:
|
|
156
|
+
field: resources.limits_set
|
|
157
|
+
op: eq
|
|
158
|
+
value: true
|
|
159
|
+
remediation: >-
|
|
160
|
+
Configure resources.limits for every container, and set a ResourceQuota / LimitRange on the namespace.
|
|
161
|
+
sources:
|
|
162
|
+
- framework: NIST AI RMF 1.0
|
|
163
|
+
reference: MEASURE 2.7 (AI system security and resilience evaluated)
|
|
164
|
+
url: https://www.nist.gov/itl/ai-risk-management-framework
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2026 The AgentEval Authors
|
|
3
|
+
"""AI system certification: automatically issue a digital certificate when both security (scan) and quality (evaluation) meet the policy gates."""
|
|
4
|
+
|
|
5
|
+
from .assess import assess_quality, assess_security, decide
|
|
6
|
+
from .issuer import (
|
|
7
|
+
build_certificate, certificate_state, gate_text, issue, load_policy, load_system_file, load_systems,
|
|
8
|
+
)
|
|
9
|
+
from .models import AISystem, CertPolicy, Decision
|
|
10
|
+
from .signing import default_key_path, key_id, load_or_create_key, load_public_key, public_pem, sign, verify
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"assess_quality", "assess_security", "decide", "build_certificate", "certificate_state", "gate_text",
|
|
14
|
+
"issue", "load_policy", "load_system_file", "load_systems", "AISystem", "CertPolicy", "Decision",
|
|
15
|
+
"default_key_path", "key_id", "load_or_create_key", "load_public_key", "public_pem", "sign", "verify",
|
|
16
|
+
]
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2026 The AgentEval Authors
|
|
3
|
+
"""Certification decision: aggregate scan and evaluation evidence within an AI system's scope and check each gate.
|
|
4
|
+
|
|
5
|
+
The security score counts only findings on the system's own assets plus organization-level (cluster) assets that
|
|
6
|
+
belong to the system's registered rule packs. Issues in other workloads of the same cluster neither hurt nor help it.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
|
|
13
|
+
from agenteval.evaluation.models import EvalRun
|
|
14
|
+
from agenteval.models import RulePack, ScanResult, Status
|
|
15
|
+
|
|
16
|
+
from .models import AISystem, CertPolicy, Decision, Gate, PackResult, QualityAssessment, SecurityAssessment
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _now() -> datetime:
|
|
20
|
+
return datetime.now(timezone.utc)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _aware(dt: datetime | None) -> datetime | None:
|
|
24
|
+
if dt is not None and dt.tzinfo is None:
|
|
25
|
+
return dt.replace(tzinfo=timezone.utc)
|
|
26
|
+
return dt
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _pct(p: int, f: int) -> float | None:
|
|
30
|
+
return round(100.0 * p / (p + f), 1) if (p + f) else None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def in_scope(system: AISystem, finding) -> bool:
|
|
34
|
+
return (finding.asset_id == system.asset_id or finding.asset_type == "cluster") and finding.pack in system.rule_packs
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def assess_security(system: AISystem, scan: ScanResult | None, packs: dict[str, RulePack],
|
|
38
|
+
policy: CertPolicy) -> SecurityAssessment:
|
|
39
|
+
sa = SecurityAssessment()
|
|
40
|
+
for pid in system.rule_packs:
|
|
41
|
+
p = packs.get(pid)
|
|
42
|
+
sa.packs.append(PackResult(id=pid, name=p.name if p else pid, name_zh=p.name_zh if p else "",
|
|
43
|
+
tier=p.tier if p else "unknown", jurisdiction=p.jurisdiction if p else ""))
|
|
44
|
+
if scan is None:
|
|
45
|
+
return sa
|
|
46
|
+
sa.scan_id, sa.scanned_at, sa.rules_version = scan.scan_id, _aware(scan.finished_at or scan.started_at), scan.rules_version
|
|
47
|
+
by_pack = {p.id: p for p in sa.packs}
|
|
48
|
+
seen_pack: set[str] = set()
|
|
49
|
+
for f in scan.findings:
|
|
50
|
+
if not in_scope(system, f):
|
|
51
|
+
continue
|
|
52
|
+
pr = by_pack[f.pack]
|
|
53
|
+
seen_pack.add(f.pack)
|
|
54
|
+
if f.status == Status.passed:
|
|
55
|
+
pr.passed += 1; sa.passed += 1
|
|
56
|
+
elif f.status in (Status.failed, Status.error):
|
|
57
|
+
pr.failed += 1; sa.failed += 1
|
|
58
|
+
if f.severity.value in policy.block_on_severities:
|
|
59
|
+
sa.blocking_findings.append(f"{f.rule_id}@{f.asset_id}")
|
|
60
|
+
elif f.status == Status.manual:
|
|
61
|
+
pr.manual += 1; sa.manual += 1
|
|
62
|
+
sa.pending.append(f"{f.rule_id}@{f.asset_id}")
|
|
63
|
+
else:
|
|
64
|
+
pr.not_applicable += 1
|
|
65
|
+
for pr in sa.packs:
|
|
66
|
+
pr.score = _pct(pr.passed, pr.failed)
|
|
67
|
+
if pr.id not in seen_pack:
|
|
68
|
+
pr.status = "not_assessed"
|
|
69
|
+
elif pr.passed + pr.failed + pr.manual == 0:
|
|
70
|
+
pr.status = "not_applicable"
|
|
71
|
+
elif pr.failed == 0 and pr.manual == 0:
|
|
72
|
+
pr.status = "passed"
|
|
73
|
+
else:
|
|
74
|
+
pr.status = "failed"
|
|
75
|
+
sa.score = _pct(sa.passed, sa.failed)
|
|
76
|
+
return sa
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def assess_quality(run: EvalRun | None) -> QualityAssessment:
|
|
80
|
+
qa = QualityAssessment()
|
|
81
|
+
if run is None:
|
|
82
|
+
return qa
|
|
83
|
+
qa.run_id, qa.evaluated_at = run.run_id, _aware(run.finished_at or run.started_at)
|
|
84
|
+
qa.exam, qa.exam_version, qa.agent_name, qa.judge = run.paper_id, run.paper_version, run.agent_name, run.judge
|
|
85
|
+
qa.score, qa.grade = run.overall_score, run.grade
|
|
86
|
+
qa.critical_failures, qa.errors = run.stats.critical_failures, run.stats.errors
|
|
87
|
+
qa.cases, qa.cases_passed = run.stats.cases, run.stats.cases_passed
|
|
88
|
+
qa.dimensions = [{"id": d.id, "name": d.name, "weight": d.weight, "score": d.score} for d in run.dimensions]
|
|
89
|
+
return qa
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def decide(system: AISystem, scan: ScanResult | None, run: EvalRun | None, packs: dict[str, RulePack],
|
|
93
|
+
policy: CertPolicy, now: datetime | None = None) -> Decision:
|
|
94
|
+
now = _aware(now) or _now()
|
|
95
|
+
sa = assess_security(system, scan, packs, policy)
|
|
96
|
+
qa = assess_quality(run)
|
|
97
|
+
g: list[Gate] = []
|
|
98
|
+
max_age = policy.max_evidence_age_days
|
|
99
|
+
|
|
100
|
+
# ---- Security (scan) ----
|
|
101
|
+
asset_seen = scan is not None and any(f.asset_id == system.asset_id for f in scan.findings)
|
|
102
|
+
g.append(Gate(code="sec_asset_scanned", ok=asset_seen, params={"asset": system.asset_id}))
|
|
103
|
+
missing = [p.id for p in sa.packs if p.status == "not_assessed"]
|
|
104
|
+
g.append(Gate(code="sec_packs_assessed", ok=asset_seen and not missing, params={"missing": missing}))
|
|
105
|
+
g.append(Gate(code="sec_score", ok=sa.score is not None and sa.score > policy.min_security_score,
|
|
106
|
+
params={"score": sa.score, "threshold": policy.min_security_score}))
|
|
107
|
+
g.append(Gate(code="sec_no_blocking", ok=asset_seen and not sa.blocking_findings,
|
|
108
|
+
params={"items": sa.blocking_findings, "severities": policy.block_on_severities}))
|
|
109
|
+
if policy.require_no_pending_attestations:
|
|
110
|
+
g.append(Gate(code="sec_no_pending", ok=asset_seen and not sa.pending, params={"items": sa.pending}))
|
|
111
|
+
scan_age = (now - sa.scanned_at).days if sa.scanned_at else None
|
|
112
|
+
g.append(Gate(code="sec_fresh", kind="freshness", ok=scan_age is not None and scan_age <= max_age,
|
|
113
|
+
params={"age": scan_age, "max": max_age}))
|
|
114
|
+
|
|
115
|
+
# ---- Quality (evaluation) ----
|
|
116
|
+
subject_ok = run is not None and run.agent_name == system.agent_name and run.paper_id == system.exam
|
|
117
|
+
g.append(Gate(code="q_run", ok=subject_ok, params={"agent": system.agent_name, "exam": system.exam}))
|
|
118
|
+
g.append(Gate(code="q_score", ok=subject_ok and qa.score is not None and qa.score > policy.min_quality_score,
|
|
119
|
+
params={"score": qa.score, "threshold": policy.min_quality_score}))
|
|
120
|
+
if policy.require_zero_critical_eval_failures:
|
|
121
|
+
g.append(Gate(code="q_no_critical", ok=subject_ok and qa.critical_failures == 0,
|
|
122
|
+
params={"n": qa.critical_failures}))
|
|
123
|
+
if policy.require_no_eval_errors:
|
|
124
|
+
g.append(Gate(code="q_no_errors", ok=subject_ok and qa.errors == 0, params={"n": qa.errors}))
|
|
125
|
+
run_age = (now - qa.evaluated_at).days if qa.evaluated_at else None
|
|
126
|
+
g.append(Gate(code="q_fresh", kind="freshness", ok=run_age is not None and run_age <= max_age,
|
|
127
|
+
params={"age": run_age, "max": max_age}))
|
|
128
|
+
demo = False
|
|
129
|
+
if policy.require_llm_judge:
|
|
130
|
+
llm = bool(qa.judge) and qa.judge != "heuristic"
|
|
131
|
+
demo = subject_ok and not llm
|
|
132
|
+
g.append(Gate(code="q_llm_judge", kind="demo" if policy.allow_demo_certificates else "blocking",
|
|
133
|
+
ok=llm, params={"judge": qa.judge}))
|
|
134
|
+
|
|
135
|
+
eligible = all(x.ok for x in g if x.kind in ("blocking", "freshness"))
|
|
136
|
+
return Decision(system_id=system.id, evaluated_at=now, eligible=eligible, demo=eligible and demo,
|
|
137
|
+
gates=g, security=sa, quality=qa)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2026 The AgentEval Authors
|
|
3
|
+
"""Certificate generation, registry loading and gate description text."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import secrets
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import yaml
|
|
13
|
+
|
|
14
|
+
from agenteval.schema import check
|
|
15
|
+
|
|
16
|
+
from agenteval.config import content_dir
|
|
17
|
+
|
|
18
|
+
from .models import AISystem, CertPolicy, Decision
|
|
19
|
+
from .signing import sign
|
|
20
|
+
|
|
21
|
+
CERT_TYPE = "AISystemAssuranceCertificate"
|
|
22
|
+
CERT_VERSION = "1.0"
|
|
23
|
+
|
|
24
|
+
GATE_TEXT = {
|
|
25
|
+
"sec_asset_scanned": {"zh": "扫描覆盖该系统资产 {asset}", "en": "Scan covers the system asset {asset}"},
|
|
26
|
+
"sec_packs_assessed": {"zh": "已评估登记的全部规则包", "en": "All registered rule packs were assessed"},
|
|
27
|
+
"sec_score": {"zh": "安全合规得分 {score}% > {threshold}%", "en": "Security compliance score {score}% > {threshold}%"},
|
|
28
|
+
"sec_no_blocking": {"zh": "无 {severities} 级未修复项", "en": "No open {severities} findings"},
|
|
29
|
+
"sec_no_pending": {"zh": "无待人工证明项", "en": "No pending attestations"},
|
|
30
|
+
"sec_fresh": {"zh": "扫描证据在 {max} 天内(当前 {age} 天)", "en": "Scan evidence within {max} days (now {age} days)"},
|
|
31
|
+
"q_run": {"zh": "存在 {agent} × {exam} 的评测记录", "en": "Evaluation exists for {agent} × {exam}"},
|
|
32
|
+
"q_score": {"zh": "评测综合得分 {score} > {threshold}", "en": "Evaluation score {score} > {threshold}"},
|
|
33
|
+
"q_no_critical": {"zh": "评测无关键项失败(当前 {n})", "en": "No critical evaluation failures (now {n})"},
|
|
34
|
+
"q_no_errors": {"zh": "评测无会话错误(当前 {n})", "en": "No evaluation session errors (now {n})"},
|
|
35
|
+
"q_fresh": {"zh": "评测证据在 {max} 天内(当前 {age} 天)", "en": "Evaluation evidence within {max} days (now {age} days)"},
|
|
36
|
+
"q_llm_judge": {"zh": "使用 LLM 裁判(当前 {judge};否则仅颁发 DEMO 证书)",
|
|
37
|
+
"en": "LLM judge used (now {judge}; otherwise a DEMO certificate only)"},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def gate_text(code: str, params: dict[str, Any], lang: str = "zh") -> str:
|
|
42
|
+
tpl = GATE_TEXT.get(code, {}).get(lang) or code
|
|
43
|
+
p = {k: ("—" if v is None else ", ".join(map(str, v)) if isinstance(v, list) else v) for k, v in params.items()}
|
|
44
|
+
try:
|
|
45
|
+
return tpl.format(**p)
|
|
46
|
+
except (KeyError, IndexError):
|
|
47
|
+
return tpl
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ---------------- Registry ----------------
|
|
51
|
+
|
|
52
|
+
def certification_dir() -> Path:
|
|
53
|
+
return content_dir("certification", "AGENTEVAL_CERT_DIR")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def load_policy(directory: Path | None = None) -> CertPolicy:
|
|
57
|
+
p = Path(directory or certification_dir()) / "policy.yaml"
|
|
58
|
+
if not p.exists():
|
|
59
|
+
return CertPolicy()
|
|
60
|
+
data = yaml.safe_load(p.read_text(encoding="utf-8")) or {}
|
|
61
|
+
check("policy", data, str(p))
|
|
62
|
+
return CertPolicy.model_validate(data)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def load_systems(directory: Path | None = None) -> dict[str, AISystem]:
|
|
66
|
+
d = Path(directory or certification_dir()) / "systems"
|
|
67
|
+
out: dict[str, AISystem] = {}
|
|
68
|
+
for f in sorted(d.glob("*.y*ml")) if d.exists() else []:
|
|
69
|
+
s = load_system_file(f)
|
|
70
|
+
if s.id in out:
|
|
71
|
+
raise ValueError(f"{f.name}: duplicate system ID {s.id}")
|
|
72
|
+
out[s.id] = s
|
|
73
|
+
return out
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def load_system_file(path: Path) -> AISystem:
|
|
77
|
+
data = yaml.safe_load(Path(path).read_text(encoding="utf-8")) or {}
|
|
78
|
+
check("system", data, str(path))
|
|
79
|
+
return AISystem.model_validate(data)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ---------------- Certificates ----------------
|
|
83
|
+
|
|
84
|
+
def add_months(dt: datetime, months: int) -> datetime:
|
|
85
|
+
m = dt.month - 1 + months
|
|
86
|
+
y, m = dt.year + m // 12, m % 12 + 1
|
|
87
|
+
days = [31, 29 if (y % 4 == 0 and (y % 100 != 0 or y % 400 == 0)) else 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
88
|
+
return dt.replace(year=y, month=m, day=min(dt.day, days[m - 1]))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def new_cert_id(now: datetime) -> str:
|
|
92
|
+
return f"AEV-{now:%Y%m}-{secrets.token_hex(4).upper()}"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def build_certificate(system: AISystem, decision: Decision, policy: CertPolicy, verify_url_template: str | None,
|
|
96
|
+
now: datetime | None = None) -> dict[str, Any]:
|
|
97
|
+
if not decision.eligible:
|
|
98
|
+
raise ValueError("Not eligible for certification")
|
|
99
|
+
now = (now or datetime.now(timezone.utc)).replace(microsecond=0)
|
|
100
|
+
cid = new_cert_id(now)
|
|
101
|
+
sec, q = decision.security, decision.quality
|
|
102
|
+
payload = {
|
|
103
|
+
"type": CERT_TYPE,
|
|
104
|
+
"version": CERT_VERSION,
|
|
105
|
+
"id": cid,
|
|
106
|
+
"demo": decision.demo,
|
|
107
|
+
"issuer": {"name": policy.issuer_name, "name_zh": policy.issuer_name_zh},
|
|
108
|
+
"subject": {
|
|
109
|
+
"system_id": system.id, "name": system.name, "name_zh": system.name_zh,
|
|
110
|
+
"organization": system.organization, "industry": system.industry,
|
|
111
|
+
"description": system.description, "description_zh": system.description_zh,
|
|
112
|
+
"asset_id": system.asset_id, "model": system.model, "agent_name": system.agent_name,
|
|
113
|
+
},
|
|
114
|
+
"assurance": {
|
|
115
|
+
"security": {
|
|
116
|
+
"score": sec.score, "threshold": policy.min_security_score,
|
|
117
|
+
"scan_id": sec.scan_id, "rules_version": sec.rules_version,
|
|
118
|
+
"scanned_at": sec.scanned_at.isoformat() if sec.scanned_at else None,
|
|
119
|
+
"checks_passed": sec.passed, "checks_failed": sec.failed,
|
|
120
|
+
"packs": [p.model_dump() for p in sec.packs],
|
|
121
|
+
},
|
|
122
|
+
"quality": {
|
|
123
|
+
"score": q.score, "threshold": policy.min_quality_score, "grade": q.grade,
|
|
124
|
+
"run_id": q.run_id, "exam": q.exam, "exam_version": q.exam_version, "judge": q.judge,
|
|
125
|
+
"evaluated_at": q.evaluated_at.isoformat() if q.evaluated_at else None,
|
|
126
|
+
"cases_passed": q.cases_passed, "cases": q.cases, "critical_failures": q.critical_failures,
|
|
127
|
+
"dimensions": q.dimensions,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
"criteria": {
|
|
131
|
+
"security_score_gt": policy.min_security_score, "quality_score_gt": policy.min_quality_score,
|
|
132
|
+
"no_open_severities": policy.block_on_severities,
|
|
133
|
+
"no_pending_attestations": policy.require_no_pending_attestations,
|
|
134
|
+
"zero_critical_eval_failures": policy.require_zero_critical_eval_failures,
|
|
135
|
+
"llm_judge_required": policy.require_llm_judge,
|
|
136
|
+
},
|
|
137
|
+
"issued_at": now.isoformat(),
|
|
138
|
+
"expires_at": add_months(now, policy.validity_months).isoformat(),
|
|
139
|
+
# Optional public verification page, e.g. "https://trust.example.com/certs/{id}"; None = offline only
|
|
140
|
+
"verify_url": verify_url_template.format(id=cid) if verify_url_template else None,
|
|
141
|
+
"disclaimer": (
|
|
142
|
+
"Issued by the AgentEval toolkit from automated technical controls, submitted attestations and a "
|
|
143
|
+
"standardized agent evaluation. It is not a government certification, a legal opinion, or a PCI DSS "
|
|
144
|
+
"validation by a Qualified Security Assessor. Rule-to-regulation mappings are pending legal review."
|
|
145
|
+
),
|
|
146
|
+
"disclaimer_zh": (
|
|
147
|
+
"本证书由 AgentEval 工具依据自动化技术控制检查、提交的证明材料与标准化 Agent 评测签发,"
|
|
148
|
+
"不是政府认证、法律意见,也不等同于 PCI DSS 合格评估师(QSA)的合规验证;规则与法规的映射尚待法务审核。"
|
|
149
|
+
),
|
|
150
|
+
}
|
|
151
|
+
return payload
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def issue(system: AISystem, decision: Decision, policy: CertPolicy, key, verify_url_template: str | None = None,
|
|
155
|
+
now: datetime | None = None) -> dict[str, Any]:
|
|
156
|
+
return sign(build_certificate(system, decision, policy, verify_url_template, now), key)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def certificate_state(envelope: dict, stored_status: str | None = None, now: datetime | None = None) -> str:
|
|
160
|
+
"""Current certificate state: revoked/superseded per the registry; otherwise based on the validity period."""
|
|
161
|
+
if stored_status in ("revoked", "superseded"):
|
|
162
|
+
return stored_status
|
|
163
|
+
now = now or datetime.now(timezone.utc)
|
|
164
|
+
exp = datetime.fromisoformat(envelope["certificate"]["expires_at"])
|
|
165
|
+
return "expired" if now > exp else "active"
|