s3-estate-calibration-auditor — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited s3-estate-calibration-auditor (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Effective-exposure audit skill for an estate of AWS S3 buckets. Takes the config layers for every bucket in the estate (public-access-block.json, bucket-policy.json, bucket-acl.json, and access-points.json where present), resolves each bucket's effective verdict by composing all four layers, then answers one question a per-layer read cannot: across 8-12 buckets that mostly READ as exposed, which one is genuinely live, and is the estate otherwise clean. It returns the live bucket as the headline, ranked by severity, with a fix, then names exactly where the bucket configs stop being able to answer the question.
Effective S3 exposure is a join across four layers that each get read wrong one at a time. A public-looking bucket policy is inert under RestrictPublicBuckets; a cross-account grant survives BPA-all-on; a public-group ACL is dead under IgnorePublicAcls but a cross-account canonical user beside it is not; a clean bucket can still be public through an access point. In an estate, the trap doubles: several buckets carry a Principal '*' policy, an AllUsers ACL, or a wide-open look that is genuinely neutralised, and exactly one bucket carries a real live grant that reads just like its neutralised siblings. This skill composes the layers per bucket instead of clearing each layer in isolation, then calibrates the estate: it does not over-flag the neutralised baits, and it does not miss the buried needle.
access, or "is anything in this account public."
effectively exposed, not just whether a Principal '*' or an AllUsers grant appears somewhere in the configs.
"but BPA is on / it is scoped" needs to be confirmed against the effective verdict rather than taken on trust.
closed" needs to be checked against the cross-account grants BPA does not touch.
It reads the static configuration of an estate of buckets: per bucket, any subset of the Block Public Access booleans, the bucket policy, the bucket ACL, and the access points. Those are S3 control-plane reads (get-public-access-block, get-bucket-policy, get-bucket-acl, list-access-points + get-access-point-policy). That is the entire input. The audit is correct and complete for what the bucket configs can tell you, and it is explicit about the rest. Reachability-on-paper is not exposure-in-fact, and every audit ends by naming the joins it cannot make:
public-read grant even when the bucket is private. Join: bucket config to its per-object ACLs.
is served publicly through a CloudFront distribution with Origin Access Control. Join: bucket to its CDN distribution.
conditional grant only matters in proportion to what the trusted account/role can do and whether it re-shares onward, which lives in that account. Join: this bucket to the identity policies of the principals it trusts.
access this config Allows. Join: bucket to its VPC-endpoint policies and the org SCPs.
which is not in the config.
A clean (deceptive-clean) estate still gets a boundary section, because a BPA-neutralised estate is not a proven-safe system: turning BPA off would expose the latent statements.
For every bucket in the estate, build the effective verdict by composing four layers. Never read one layer alone. A finding is LIVE only when a real public or cross-account grant survives the layer that would neutralise it:
policy and ACL grants, but do NOT touch cross-account grants.
other account, or '*' narrowed by a Condition.
AuthenticatedUsers public groups.
independent of (but not exceeding) the bucket.
The estate is clean iff no bucket is live. The needle is whichever bucket carries a live finding among many neutralised/scoped lookalikes.
Before any judgment, read each layer for each bucket. Process EVERY bucket in the estate, not the first couple:
public-access-block.json. An absent file meansall four are False (no BPA). The two that neutralise existing grants are RestrictPublicBuckets / BlockPublicPolicy (for a public policy) and IgnorePublicAcls (for a public-group ACL). BlockPublicAcls only blocks new public ACLs and does not disable an existing one.
Statement in bucket-policy.json. A Deny grants nothing andcannot make a bucket public; classify only the Allow statements. For each Allow, read the Principal (public '', a named AWS account, or '' with a Condition) and the Condition.
bucket-acl.json. A Grantee that is the AllUsers orAuthenticatedUsers Group URI is a public-group grant; a CanonicalUser that is not the bucket owner is a cross-account grant; an owner-only ACL produces nothing.
access-points.json. Each AP has its OWNPublicAccessBlock and Policy; resolve the AP policy exactly like a bucket policy, against the AP's own BPA.
Recognise the BPA switches by name, and recognise a narrowing Condition: aws:PrincipalOrgID, aws:PrincipalOrgPaths, aws:PrincipalAccount, aws:PrincipalArn, aws:SourceArn, aws:SourceAccount, aws:SourceVpc, aws:SourceVpce, aws:SourceIp, aws:VpcSourceIp, sts:ExternalId, and the access-point delegation keys s3:DataAccessPointAccount / s3:DataAccessPointArn / s3:AccessPointNetworkOrigin. Any of these on a Principal '*' scopes it to a bounded caller set.
Read each bucket's BPA booleans verbatim — do not let a bucket's name or its grants tell you what they are. Each boolean being true is the safe direction: IgnorePublicAcls: true means an existing public ACL is ignored (dead); RestrictPublicBuckets: true means a public policy is denied. Do not invert it. The dominant miscalibration is asserting a boolean value to fit an expectation: a bucket named exports, public, share, partner, or cdn, or any bucket carrying an AllUsers / AuthenticatedUsers grant, invites the assumption that it must be the exposed one — and that assumption makes you misread its IgnorePublicAcls as false. In these estates the common case is the opposite: a shareable-sounding bucket with an AllUsers grant and IgnorePublicAcls: true, which is neutralised, not live. The presence of a grant is not evidence about the boolean. To keep the transcription faithful: for any bucket carrying an AllUsers / AuthenticatedUsers ACL grant or a Principal '*' policy, paste that bucket's entire `public-access-block.json` as a verbatim JSON block before you classify it, and read the four booleans out of the pasted block. Pasting the raw object is harder to get wrong than filling a value in from memory, which is where the misread creeps in.
Compose the layers; do not condemn a bucket on "Principal '*' is present" alone, and do not clear it on "BPA is on" alone. The codes:
narrowing Condition, and BPA is NOT restricting (RestrictPublicBuckets and BlockPublicPolicy both off). Live public exposure: anyone on the internet can perform the granted actions.
no Condition and the AP's own BPA is not restricting. Data is reachable publicly through the access point even when the bucket policy and bucket BPA are clean. Auditing only the bucket misses this.
NOT public, so BPA does not govern it: a cross-account grant stays fully live even with all four BPA switches on. The classic misread is seeing BPA-all-on and calling the bucket locked down.
bucket owner. IgnorePublicAcls neutralises the public GROUPS, not a named canonical user, so this grant survives BPA-all-on. A TLS-only Deny in the policy does not address it.
group and IgnorePublicAcls is OFF. Live public via ACL, independent of the bucket policy.
These five are the only codes that count as live exposure. The next three READ as exposed but are NOT live, and must never be reported as a live public/exposed bucket:
RestrictPublicBuckets / BlockPublicPolicy renders inert. Effective verdict: NOT public. Note it as latent risk (turning BPA off would expose it), not as live exposure.
IgnorePublicAcls makes ineffective. Effective verdict: NOT public via this ACL.
source account or VPC / ExternalId / access-point delegation). This is conditional or org-scoped sharing, NOT public: a caller outside the condition is denied. Reading Principal '*' and stopping there is the misread this guards against.
On a NEEDLE estate, name the ONE genuinely live bucket as A (the) PRIMARY finding, with the reason it is live, instead of burying it among the neutralised lookalikes or missing it. The live bucket reads just like its neutralised/scoped siblings; the pass is naming exactly that one and why:
neutralises public grants.
scoped-looking siblings carry an org id / ExternalId / SourceIp.
beside it are correctly ignored.
State the live bucket by name, the code, and the layer it is grounded in.
This is the half the naive read gets wrong in the other direction. An estate where every bucket is neutralised or scoped is CLEAN, and the audit must say so instead of manufacturing a finding. The composition in step 2 is what proves it. Specifically:
public bucket.
Do not report it as a public bucket.
sharing, not public. Do not read Principal '*' and call it public.
correctly-neutralised buckets.
On a clean estate the audit reports: NO live exposure anywhere, why the exposed-looking buckets are neutralised or scoped (the BPA switch or the Condition), and the boundary. It does not invent a critical. Noting the neutralised statements as latent / defence-in-depth is fine; asserting live public exposure is not.
Order findings by severity (critical for a public policy or public access point, high for a cross-account or public-group grant). Rank the live needle as the headline; do NOT headline a neutralised/scoped bucket, and on a clean estate do not invent a critical. For each finding: the bucket and layer it is grounded in, what the exposure is, and the fix. Then list the boundary from "What this skill reads." A clean estate still gets a boundary section.
Fix the live bucket; do not rip out intentional scoped sharing or the BPA-neutralised buckets as if they were live leaks:
the specific accounts/roles that need access. If public read is genuinely intended (a static site), front it with CloudFront + Origin Access Control instead of a public bucket or access point, and turn RestrictPublicBuckets on.
the actions are minimal; scope to specific prefixes and prefer an aws:PrincipalOrgID / sts:ExternalId condition over a bare account root. For an ACL grant, express the sharing as a scoped bucket policy and disable ACLs with Bucket Owner Enforced. BPA-all-on does not make a cross-account bucket safe.
prefer bucket policies over ACLs.
no live fix. As defence in depth, optionally remove the latent public statement or ignored ACL so the bucket does not depend on BPA staying on as its only guardrail; for a scoped bucket, confirm the condition value (the org id, the ExternalId, the SourceIp CIDRs) is the intended one and leave the grant in place if the scope is correct.
| Severity | Meaning |
|---|---|
| critical | Live public exposure: a Principal '*' bucket policy with BPA not restricting (POLICY-PUBLIC), or a public access-point policy (AP-PUBLIC). |
| high | Live cross-account or public-group exposure that BPA does not close: cross-account policy (XACCT-POLICY), cross-account canonical-user ACL (XACCT-ACL), public-group ACL with IgnorePublicAcls off (ACL-PUBLIC). |
| low | A Principal '*' scoped by a Condition (COND-SCOPED): conditional sharing to verify, not public exposure. |
| info | A neutralised grant present but inert (POLICY-PUBLIC-BLOCKED, ACL-PUBLIC-IGNORED): latent risk if BPA is turned off, not live exposure. |
Only the critical and high bands are LIVE exposure and count toward the estate verdict. The low and info bands are exposed-looking-but-not-live; they are notes, never the headline.
| Code | Rule | Severity | Live | Grounded in |
|---|---|---|---|---|
| POLICY-PUBLIC | Bucket policy Principal '*', no Condition, BPA not restricting | critical | yes | bucket policy x BPA |
| AP-PUBLIC | Access-point policy Principal '*', AP BPA not restricting | critical | yes | access-point policy x AP BPA |
| XACCT-POLICY | Bucket policy grants a named other account | high | yes | bucket policy (BPA does not touch it) |
| XACCT-ACL | Bucket ACL grants a non-owner canonical user | high | yes | bucket ACL (IgnorePublicAcls does not touch it) |
| ACL-PUBLIC | Bucket ACL grants AllUsers / AuthenticatedUsers, IgnorePublicAcls off | high | yes | bucket ACL x BPA |
| COND-SCOPED | Principal '*' narrowed by a Condition | low | no | bucket policy Condition |
| POLICY-PUBLIC-BLOCKED | Principal '*' policy neutralised by RestrictPublicBuckets / BlockPublicPolicy | info | no | bucket policy x BPA |
| ACL-PUBLIC-IGNORED | Public-group ACL neutralised by IgnorePublicAcls | info | no | bucket ACL x BPA |
The matching half of every live rule is the clean verdict: POLICY-PUBLIC neutralised to POLICY-PUBLIC-BLOCKED, ACL-PUBLIC neutralised to ACL-PUBLIC-IGNORED, a Principal '*' scoped to COND-SCOPED, on an estate of these is the correct, complete output, not a failure to find something. Reporting a neutralised bucket as a live leak is the dominant failure mode this skill prevents.
The agent's final message in any invocation must include:
in, what the exposure is, and the fix. The live needle named explicitly as the headline. Or "no live exposure" for a deceptive-clean estate, stating why the exposed-looking buckets are neutralised or scoped.
fronting, the trusted principals' identity policies, the account-level BPA dependency, VPC endpoint policies / org SCPs, data sensitivity), stated explicitly so the gap is visible instead of silent.
Seven end-to-end fixtures are committed under fixtures/, each an estate of 8-12 buckets with a runnable replay test. The set is deliberately weighted toward deceptive-clean, because over-flagging a neutralised estate is the cold agent's dominant failure here. No loud, obvious public bucket appears: the base model already aces those.
05-logging-estate-needle: the needle. acme-log-shippinghas all four BPA switches on (reads as locked down) but its policy grants a named other account read/list. A cross-account grant survives BPA: XACCT-POLICY (high), live.
06-analytics-estate-needle: acme-analytics-clickstreamgrants Principal '*' GetObject with NO Condition and BPA not restricting, sitting next to siblings that carry an org id / ExternalId or have RestrictPublicBuckets on: POLICY-PUBLIC (critical), live.
07-partner-share-needle: acme-share-partner-dropgrants READ to another account's canonical user via its ACL; IgnorePublicAcls (on for this estate) only ignores the AllUsers lookalikes beside it: XACCT-ACL (high), live.
01-media-platform-clean: an AllUsers ACL, a Principal'*' policy, org/IP-scoped policies, and an access-point delegation, all neutralised or scoped. Clean.
02-data-lake-clean: public-looking policies and a publicACL, all neutralised by BPA or scoped by org-path / external-id. Clean.
03-saas-tenancy-clean: tenant-shared buckets scoped byorg id / ExternalId, plus one ignored AllUsers ACL. Clean.
04-backup-estate-clean: BPA-neutralised policies, anignored public ACL, and a SourceIp office allowlist. Clean.
Every fixture has a replay test in tests/ that runs the methodology (via the deterministic reference engine tests/_resolve.py, aggregated across the estate by tests/_estate.py) against the committed JSON, with no external credentials. Run from the skill directory:
for t in tests/replay_*.py; do python "$t" || exit 1; doneThe seven tests cover the three needle estates (the one live code fires, on the named bucket) and the four deceptive-clean estates (no live finding fabricated). Tests exit non-zero if the audit names the wrong bucket or invents one on a clean estate. See tests/README.md for the fixture schema.
This skill is wrong in predictable ways. Read FAILURE_MODES.md before relying on it. Highlights:
with no sensitive objects, or one fronted by a CDN, or one whose trusted account does nothing with it. Exposure-on-paper is a hypothesis to confirm, not a breach.
from live; "clean" means clean today, not proven-safe.
distribution, a per-object grant, or an account-level BPA the per-bucket config does not carry is outside the graph this skill builds.
The audit above runs end-to-end against the bucket-config JSON the user already has. No Anyshift dependency.
Every boundary note in this skill is a join: bucket to its per-object ACLs, bucket to its CloudFront distribution, bucket to the identity policies of the principals it trusts, bucket to its VPC-endpoint policies and org SCPs, estate to the account-level BPA the neutralisation depends on. The Anyshift MCP can act as a context primer by resolving those joins from a versioned resource graph, so an XACCT-POLICY finding ("cross-account, real only if the trusted account is privileged or re-shares") can be closed instead of deferred at the boundary. A measured "with vs without" delta will be published here once the integration has been exercised against the replay fixtures.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.