Evidence & Reports
Developer Reference
This page covers internal implementation details. It is not included in the User Guide.
Evidence is a first-class, append-only, citable record. It attaches to Report Items, Findings, Plans, Bundles, and Runs. Every Evidence record carries native cloud-console deep links so any claim can be verified in one click in the source-of-truth system.
Design intent vs shipped
This page documents the target Evidence model — Option A from the design review: Evidence attaches to Report Items directly (not only to Findings). Mandatory console_links[] is part of the schema.
What's verified shipped today:
- The v3 Appendix A canonical model says Evidence is first-class and attaches to Findings / Plans / Runs.
- The v4 Analysis Agent semantic-payload spec references
evidence_refs[]andevidence_idfields.
What's not yet implemented (in the running CE API per context_engine_api.py):
- An
Evidencecollection or/resolve/evidenceendpoint - The console-link generation module
- Per-Report-Item Evidence linking
- The full export pipeline described in the Export section below
Treat this page as the design contract. Engineering needs to confirm storage location, retention, and signing before any of this lands in code.
Where Evidence attaches
Report → Report Item → Evidence record(s)
+ EstateView pin
Finding → the Report Item it derives from
+ additional Evidence captured during triage
Plan → one or more Findings
+ Plan-time Evidence (assumptions, blast-radius reads)
Bundle → one or more Plan revisions
+ Bundle-time Evidence (current state at materialisation)
Run → the Bundle
+ Run-time Evidence (before / after state, API calls, approvals)Every artifact at every stage has its own Evidence chain. Re-running a Report on a fresher EstateView produces a new Report version with a fresh evidence chain; old versions are retained immutably.
Evidence record schema
evidence:
id: ev_01HX... # immutable ULID
schema_version: 1
type: cloudtrail_event # enum, see below
captured_at: 2026-05-13T09:14:22Z
estate_view_id: ev_42 # which EstateView snapshot this belongs to
source:
system: aws.cloudtrail # aws.* | azure.* | github.* | jira.* | linear.* | internal.*
region: us-east-1 # nullable for global services
account_id: "123456789012" # AWS account / Azure subscription / GCP project
tenant_id: "11111111-2222-..." # Azure-only (Microsoft tenant)
native_id: "e-1234-..." # provider-side ID (event ID, ARN, resource ID)
api_call: "lookup-events" # the call Escher made to capture this
console_links: # MANDATORY: at least one entry
- label: "View event in CloudTrail"
href: "https://us-east-1.console.aws.amazon.com/cloudtrail/home?region=us-east-1#/events/e-1234-..."
- label: "View bucket in S3 Console"
href: "https://us-east-1.console.aws.amazon.com/s3/buckets/prod-customer-uploads"
payload: # raw, type-specific content. JSON-serialisable. Immutable.
# shape depends on `type` — examples below
summary: "IAM user 'bot-deploy' rotated its access key at 09:13 UTC."
attached_to:
- report_item_id: ri_...
- finding_id: fnd_...
- plan_id: pln_...
- run_id: run_...
redaction_class: standard # standard | sensitive | pii — controls export maskingType enum (closed set)
Adding a new type requires a schema_version bump.
| Type | Captures | source.system examples |
|---|---|---|
cloudtrail_event | A single AWS CloudTrail event | aws.cloudtrail |
azure_activity_log | A single Azure Activity Log entry | azure.activitylog |
config_snapshot | Full configuration of a specific resource at a moment | aws.s3, aws.ec2, azure.storage |
iam_policy | A policy document (inline or managed) | aws.iam, azure.rbac |
billing_line | A line item from Cost Explorer / Cost Management | aws.ce, azure.costmanagement |
metric_point | A single metric value at a timestamp | aws.cloudwatch, azure.monitor |
log_line | A single log line from CloudWatch / Azure Monitor logs | aws.cloudwatch.logs, azure.loganalytics |
deployment_record | A deployment event (CI/CD trigger, commit, PR) | github.* |
pr_record | A pull request's metadata + diff summary | github.pullrequest |
commit_record | A specific commit | github.commit |
approval_record | A human approval action (in-app, Slack, webhook) | internal.approval |
api_call | An Escher-made API call with its request + response | internal.api_log |
manual_note | Human-attached context, comment, or assumption | internal.note |
Cloud console URL patterns
Console links are generated at evidence-capture time by a pure function over (type, source, native_id). No extra API call needed.
AWS
| Resource | URL pattern |
|---|---|
| S3 bucket | https://{region}.console.aws.amazon.com/s3/buckets/{bucket}?region={region} |
| S3 object | https://{region}.console.aws.amazon.com/s3/object/{bucket}?region={region}&prefix={key} |
| EC2 instance | https://{region}.console.aws.amazon.com/ec2/home?region={region}#InstanceDetails:instanceId={id} |
| Security group | https://{region}.console.aws.amazon.com/ec2/home?region={region}#SecurityGroup:groupId={sg_id} |
| IAM user | https://us-east-1.console.aws.amazon.com/iam/home#/users/{name} |
| IAM role | https://us-east-1.console.aws.amazon.com/iam/home#/roles/{name} |
| IAM policy | https://us-east-1.console.aws.amazon.com/iam/home#/policies/{arn}$jsonEditor |
| RDS instance | https://{region}.console.aws.amazon.com/rds/home?region={region}#database:id={id};is-cluster=false |
| Lambda function | https://{region}.console.aws.amazon.com/lambda/home?region={region}#/functions/{name} |
| CloudTrail event | https://{region}.console.aws.amazon.com/cloudtrail/home?region={region}#/events/{event_id} |
| Cost Explorer | https://us-east-1.console.aws.amazon.com/cost-management/home#/cost-explorer |
| Config rule | https://{region}.console.aws.amazon.com/config/home?region={region}#/rules/details?configRuleName={name} |
Notes:
- IAM, Cost Explorer, and Organizations are global services — region segment is always
us-east-1for these URLs (AWS convention). - The console requires the user to already be signed into the right AWS account in their browser. Multi-account environments can prepend a sign-in URL:
https://signin.aws.amazon.com/switchrole?account={account_id}&roleName={role}&destination={encoded_url}.
Azure
Base portal URL: https://portal.azure.com/#@{tenant_id}
| Resource | URL pattern |
|---|---|
| Subscription overview | https://portal.azure.com/#@{tenant_id}/resource/subscriptions/{sub_id}/overview |
| Resource (generic) | https://portal.azure.com/#@{tenant_id}/resource/subscriptions/{sub_id}/resourceGroups/{rg}/providers/{provider}/{type}/{name} |
| Storage account | https://portal.azure.com/#@{tenant_id}/resource/subscriptions/{sub_id}/resourceGroups/{rg}/providers/Microsoft.Storage/storageAccounts/{name}/overview |
| Virtual machine | https://portal.azure.com/#@{tenant_id}/resource/subscriptions/{sub_id}/resourceGroups/{rg}/providers/Microsoft.Compute/virtualMachines/{name}/overview |
| NSG | https://portal.azure.com/#@{tenant_id}/resource/subscriptions/{sub_id}/resourceGroups/{rg}/providers/Microsoft.Network/networkSecurityGroups/{name}/overview |
| Activity log | https://portal.azure.com/#@{tenant_id}/resource/subscriptions/{sub_id}/activityLog |
| Cost management | https://portal.azure.com/#@{tenant_id}/resource/subscriptions/{sub_id}/costmanagement |
Notes:
- Tenant ID is required for any Azure portal link. Captured during Connect Azure (the
az loginstep) and stored on the profile. - Subscription-level pages don't need a resource group.
- For multi-tenant users, the
@{tenant_id}segment routes to the correct tenant on portal load.
GCP (roadmap)
GCP support is not shipped today (see Connect GCP). When it ships, console URLs will follow https://console.cloud.google.com/{service}/{resource}?project={project_id} patterns.
How console_links surface across the product
| Surface | Rendering |
|---|---|
| Canvas — inline claim | Citation chip on the claim. Click → drawer opens with summary, payload preview, and console_links rendered as buttons at the top of the drawer (e.g. "View bucket in S3 Console ↗") |
| Canvas — PDF export | Each Evidence record renders as a footnote at the bottom of the page. console_links render as labelled URLs (printable + clickable in PDF readers) |
| JSON export | console_links[] array preserved in full |
| Markdown export | Inline link under each cited claim: [View bucket in S3 Console](https://...) |
| Slack notification | Up to 2 console links surfaced as Block Kit buttons; "+N more" overflow |
| Jira / Linear ticket | All console links listed in the ticket description under "Verify in cloud:" |
Export modes
Three formats. Redaction class is chosen at export time.
| Format | Best for | What's in it |
|---|---|---|
| PDF (audit pack) | External auditors, board readouts | Cover page (tenant, EstateView pin, generated_at, scope, redaction class) → claims with footnoted citations → appendix with full evidence records. Console links printed + clickable. |
| JSON (SIEM / GRC ingest) | Splunk, Datadog, custom GRC tools | Top-level Findings array with evidence_refs[]. Separate evidence[] array so duplicate references are stored once. Bulk audit pack = ZIP of manifest.json + evidence/{id}.json + findings/{id}.json + report-summary.pdf. |
| Markdown (tickets / wikis) | Drop into Jira description, Notion page, GitHub Issue | Claims as bullets, evidence as expandable <details> blocks. Useful for collaboration; less suitable for formal audit. |
Redaction model
| Class | What's masked |
|---|---|
standard (default) | Nothing — full evidence with native IDs and console links |
sensitive | Account IDs, IPs, IAM principal names, billing amounts. Console links rewritten to the generic service home (e.g. https://console.aws.amazon.com/s3/) — external auditor can't pivot from the link |
pii | Everything in sensitive + masks email-looking strings, phone-number-looking strings, and tag values flagged as containing PII |
Redaction is applied at export time. The underlying Evidence record is always stored at standard class — redaction is a view, not a destructive transform.
Reports vs Evidence — clean boundary
| Report | Evidence | |
|---|---|---|
| What it is | Snapshot of observed data — a list of Report Items against a pinned EstateView | Immutable proof for a specific claim — typed, source-attributed, console-linked |
| Mutability | New versions can be generated; old versions retained | Append-only, never modified |
| Citations | Cites its EstateView pin + per-Report-Item Evidence records | n/a — Evidence is the citation |
| Lifecycle | Generated by scans or by user request; versioned over time | Created at the moment the cited fact is observed; never deleted |
Reports cite Evidence. Findings, Plans, Bundles, Runs cite Evidence. Evidence cites the cloud.
Open items requiring engineering sign-off
- Where Evidence records live — new CE collection (
escher_evidence_<tenant_id>) or a separate write-optimised store? Volume profile is millions of immutable records per tenant per quarter. - Signing. Per-record HMAC, per-export PGP, or per-bundle X.509? "Signed evidence bundles" was previously claimed as Enterprise — what's the actual implementation path?
- Retention windows. Defaults per tier — legal sign-off needed.
- Console-link generator module location. Lives next to the type enum so they evolve together.
- Azure tenant-ID capture. Captured at
az logintime during Connect Azure and stored on the profile.
Next steps
- Schema Reference — where the Evidence collection will be documented when it lands in CE
- Executing Playbooks — how Evidence is created during a Run
- Compliance Ops Skill — how compliance frameworks consume Evidence