本文へ移動
cccskills
無料GitHub で公開

cloud-saas-exposure

Organization-grade cloud and supply-chain attack-surface discovery: S3/GCS/Azure Blob bucket discovery via observed-name mining (CNAME/cert-SAN/Wayback) and bounded two-class permutation (6 prefixes x 15 suffixes on trusted tokens, bounded target-bound expansion on subdomain stems), existence (HEAD/GET) vs public-listing confirmation, object-key triage into 9 value tiers (database dumps, credentials, IaC state, kubeconfig, VCS dirs, config, archives, PII, logs), dangling-CNAME bucket-takeover detection, and the ownership-gated severity model that stops an unattributable public bucket from becoming a false CRITICAL; the fully offline AWS-account-ID recovery from a leaked AKIA/ASIA/AROA access key (base32 decode, runnable stdlib Python, canonical test vector, AWS-documentation-example-ID screening); dependency-confusion confirmation for npm/PyPI (internal-signal classifier -- private-registry binding vs org-namespace match -- paired with a read-only public-registry 404 check and the npm scope-claimability nuance the public search API misses); and passive cloud-native/container/Kubernetes/CI control-plane fingerprinting (Lambda URLs, API Gateway, Cloud Run, App Service, kubelet/etcd/K8s API/dashboard, Jenkins/GitLab/Argo CD) as an org-attribution and exposure surface. Passive/discovery only -- no exploitation, no credential submission, no active control-plane confirmation (a stage-6 validate_cloud active tier is described but out of scope). Use when enumerating a target's cloud storage footprint, recovering an AWS account ID from a leaked key, confirming a supply-chain dependency-confusion vector, or fingerprinting cloud-native/K8s/CI infrastructure for an authorized external recon engagement.

インストール方法を見る

含まれるファイル(2)

  • SKILL.md43.7 KB
  • README.md7.0 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Cloud & SaaS Exposure — Buckets, Offline AWS Account-ID Recovery, Dependency Confusion, and Cloud-Native/K8s Fingerprinting

Companion skills: osint-methodology (the pipeline this plugs into — Stage 2 asset expansion, Stage 4 exposure analysis, Stage 5 supply-chain confirmation). offensive-osint §16.8 (bucket-permutation raw wordlist), §16.17 (cloud-native URL pattern table), §16.18– §16.19 (container/K8s/CI paths + active curl recipes), §44 (package-registry search). This skill does not repeat those lists — it builds the reasoning layer on top: the ownership-gated bucket severity model, the offline AKIA→account-ID decode, the dependency-confusion two-part confirmation contract, and cloud-native/K8s as an org-attribution surface, not just another probe list.

0. When to Use / When NOT

Use this skill when: you're mapping an authorized target's cloud and supply-chain footprint — enumerating storage buckets and judging whether a hit is actually the target's risk (not a stranger's public bucket that happens to match a permutation); recovering the AWS account ID behind a leaked access key you already hold (dead or live); confirming whether an internal-looking npm/PyPI dependency is a registerable supply-chain vector; or fingerprinting cloud-native (Lambda/Cloud Run/App Service/…) and container/K8s/CI control-plane surface for org attribution and exposure triage.

Do NOT use this skill when: you just need the raw bucket-permutation wordlist, cloud-native URL pattern table, or container/K8s/CI path list with no reasoning layer — go straight to offensive-osint §16.8/§16.17–16.19/§44. Do NOT use it for anything past discovery/confirmation: registering an unclaimed package, submitting AWS credentials, authenticating to a Kubernetes API, or confirming a fingerprinted control plane actually answers unauthenticated (that's a stage-6 --validate --validate-cloud active tier — described, never performed, §5/§9.4).


1. Authorization & Legal Posture

Reuses osint-methodology §1 — assets you own or have written authorization to assess. Three of this skill's four subsystems carry a distinct authorization shape, worth being explicit about before you run any of them:

  • Bucket probing (§6) is a real HTTP GET against bucket infrastructure that may or may not be the target's — the same "active but low-intrusion" tier as any other GET against target-adjacent infra. Standard engagement authorization applies.
  • AWS account-ID decode (§7) is fully offline. No authorization question beyond already lawfully holding the leaked key.
  • Dependency-confusion confirmation (§8) issues live GETs, but only against the public npm/PyPI registries — zero packets to the target. This is why it's explicitly in scope even though it's "active."
  • Cloud-native/K8s/CI fingerprinting (§9) is pattern-matching over hostnames and ports already resolved by earlier recon — no new network traffic of its own.

2. Confidence Levels

Reuses osint-methodology §2. Domain-specific anchors:

LevelCloud/SaaS example
TENTATIVEOrg-namespace-matched dependency name (medium strength) with no private-registry binding — namesake-prone.
FIRMBucket exists (403/private) and is name-tied or CNAME/SAN/Wayback-observed; AWS account ID offline-decoded from a leaked key with no live validation; cloud-native endpoint pattern-matched via an owned CNAME/cert-SAN/subdomain FQDN; a container/K8s orchestration port is open (directly observed — its auth posture is not).
CONFIRMEDBucket is publicly listable/readable and ownership-verified; AWS account ID corroborated by a live STS-validated key; dependency-confusion "strong" signal (private-registry binding) confirmed unclaimed via the public-registry 404 + scope-claimability check.

An unattributable public bucket hit does not get a confidence label at all — it never reaches the finding stream (§6.3).


3. Output Format

Finding:
  id:          <stable hash or UUID>
  module:      <technique that discovered it>
  asset_key:   <typed key, e.g. bucket:s3:acme-backup, account:aws:609629065308>
  category:    <PUBLIC_BUCKET | INFO_DISCLOSURE | TAKEOVER | DEPENDENCY_CONFUSION | MISCONFIG | OPEN_SERVICE | EXPOSED_PANEL>
  severity:    <info|low|medium|high|critical>
  confidence:  <tentative|firm|confirmed>
  title:       <one-line summary>
  description: <2-5 sentences — issue + attacker impact, not a terse restatement of the title>
  evidence:
    url:       <where found>
    timestamp: <UTC ISO8601>
    sha256:    <hash of any downloaded artifact — never an object body, see §5>
    raw:       <truncated to 2 KiB>
  references:  [<advisory URL, vendor doc>]
  remediation: <action the asset owner can take>

UTC timestamps everywhere.


4. Source Hygiene & Citations

URL + UTC timestamp + SHA-256 + tool version + run_id, every artifact. For bucket listings, hash the listing response (the XML/JSON), never an object's contents — this skill never fetches an object body (§5). For a decoded AWS account ID, cite the key string it was derived from (redacted to first/last 4 chars in client-facing output) and the algorithm version. For a dependency-confusion hit, cite both registry-check timestamps (the package 404 and, for scoped npm, the scope-ownership 404) — claimability is a point-in-time fact that can flip the moment someone else registers the name.


5. Do NOT

  • Do NOT fetch or download the contents of any object inside a bucket. Listing keys / sampling object names from the listing response is in scope (§6.3); retrieving an object's body is not — the reference implementation gates raw object reads behind --validate.
  • Do NOT claim CRITICAL severity for a bucket, endpoint, or account hit that isn't ownership-verified. An unattributable public hit is not the client's risk — see the ownership-gated model, §6.3.
  • Do NOT register, reserve, or publish a package name found unclaimed by the dependency-confusion check. Confirmation only — §8.7.
  • Do NOT use a decoded/derived AWS account ID to call AWS APIs (sts:AssumeRole, GetCallerIdentity) against real infrastructure, with your own or the target's credentials. That reverse step needs the operator's own AWS credentials against a third party's account, is CloudTrail-logged on the target's side, and is out of this skill's scope entirely — describe the pivot value (§7.1), never perform it.
  • Do NOT actively probe or authenticate against a fingerprinted Kubernetes API, etcd, kubelet, or Docker daemon endpoint to confirm its auth posture. Passive fingerprint only — live confirmation is a stage-6 --validate --validate-cloud active tier, out of scope.
  • Do NOT single-source attribute a bucket/account/dependency/endpoint to the target — apply the rule of three (osint-methodology §2) or the explicit ownership signals in §6.3/§7.7/§9.1.

6. Storage Bucket Discovery — Candidate Generation, Existence vs. Listing, and the Ownership-Gated Severity Model

6.1 Two-class candidate generation

Two distinct expansion classes — mixing them either misses brand buckets or produces a candidate storm (an unconstrained full-expansion-per-subdomain-stem approach measured at ~95k candidates on a large-subdomain target).

Class A — trusted tokens (apex root, the domain with dots replaced by hyphens, the domain with dots stripped, and a sanitized --company token if supplied): full prefix × suffix expansion.

6 prefixes: "" (bare) backup- assets- static- dev- prod-

15 suffixes: "" (bare) -backup -assets -static -media -data -uploads -dev -prod -staging -logs -private -public -dump -archive

→ up to 90 candidates per trusted token.

Class B — subdomain stems (the first label of every discovered subdomain, excluding www/mail/ns1/ns2): bounded and target-bound only —

  • Bare probe, but only when the stem is distinctive: not in the broad (~90-entry) generic-stem filter (api, admin, backup, dev, staging, mail, data, docs, internal, vault, secure, sandbox, preprod, … — a stricter, larger list than the 47-word variant in offensive-osint §16.8) and longer than 3 characters — or the stem already contains a trusted token.
  • Target-bound permutation only: {apex_root|company}-{stem} and {stem}-{apex_root|company} (hyphen joiner, both orders).

No standalone prefix/suffix expansion is ever applied to a subdomain stem. Every candidate must satisfy the bucket-name shape ^[a-z0-9][a-z0-9.\-]{1,61}[a-z0-9]$ (3–63 characters).

Observed-name mining bypasses the filter entirely. A bucket name mined from the target's own DNS, certificate, or archived pages is near-certain to be theirs, so it's probed regardless of the rules above:

  • subdomain CNAME chains resolving to *.s3(.<region>).amazonaws.com, *.storage.googleapis.com, or *.blob.core.windows.net
  • certificate SANs matching the same three host patterns
  • archived (Wayback) URLs, path- or vhost-style, referencing an S3/GCS/Azure Blob host

6.2 Probe technique — existence vs. listing

One GET per candidate per provider — no separate HEAD, since the GET body is needed anyway to classify listing:

ProviderURLListing marker (200)Exists, no listingNot found
S3GET https://{name}.s3.amazonaws.com/<ListBucketResult → LISTABLE200/301/307 (no marker) or 403 → exists (403 = private; parse <Code> for the deny reason)NoSuchBucket (404) → absent
GCSGET https://storage.googleapis.com/{name}<ListBucketResult → LISTABLE403 → exists, private404 → absent
Azure BlobGET https://{name}.blob.core.windows.net/?comp=list<EnumerationResults → LISTABLE400/403 → account exists, private404 → absent

S3 region redirects (301/307) are followed once before final classification. On a listable hit, sample up to 200 object keys straight out of the listing body already returned (<Key>…</Key> for S3/GCS, <Blob><Name>…</Name> for Azure) — never a second request per object, never a fetch of an object's contents.

6.3 The ownership-gated severity model

This is the discipline that stops a permutation hit on a stranger's globally-registered bucket from becoming a false CRITICAL — the exact class of false positive an earlier, unconstrained version of this logic produced: unverified public/listable hits emitted at MEDIUM/LOW severity were the documented source of a 171-finding false-positive flood before this gate was added. (A related, separate incident — filing a merely-exists-but-private 403/400 bucket as a PUBLIC_BUCKET exposure — produced a "33 listable buckets" false read on a fully-private estate; §6.2's reachability classification is what fixed that one.) Two independent axes decide the outcome — what's reachable and whether ownership is verified — and the outcome is no finding at all, not a downgraded severity, whenever the second axis fails.

Ownership verified means at least one of:

  • name-tied — the candidate name contains a trusted token (apex root / domain variant / --company)
  • observed — the name was mined from the target's own CNAME, cert SAN, or archived URL (§6.1)
  • branded object key — on a listable bucket, at least one returned object key itself names the target (checked after listing, so this can promote an otherwise-generic name)
ReachabilityOwnership unverifiedOwnership verified
Listable (public listing)No finding. Recorded as a Bucket asset only.PUBLIC_BUCKET, CRITICAL, CONFIRMED
Public-read, not listableNo finding. Asset only.PUBLIC_BUCKET, MEDIUM, CONFIRMED
Exists, access denied (403/400)No finding. Asset only.INFO_DISCLOSURE, INFO, FIRM
Not found (404)Nothing — unless a live subdomain CNAME still points at the absent name (§6.4)—

The unverified rows are a deliberate zero, not a soft downgrade to LOW/INFO. A generic permutation like static-delivery or logs-archive matches thousands of unrelated companies' real, live buckets globally; surfacing every one as the client's finding is exactly what this model exists to prevent. The candidate is still recorded as a graph asset for correlation and re-probe on a future scan — it simply never enters the finding stream unattributed.

Object-key triage on a listable bucket — classify the exposed keys into value tiers so the write-up states what is exposed, not merely that listing is open. Pure filename-pattern match over the listing already returned — no object body is ever fetched:

CategorySeverity tierPattern examples
Database dumpcritical*.sql[.gz|.bz2|.zip], *dump*, *-backup*/*_backup*, *.bak, *.mdb, *.sqlite[3], *.dmp
Credentialscritical.env*, id_rsa, id_ed25519, *.pem, *.p12, *.pfx, *.kdbx, *.ppk, *.jks, *credential*
IaC statecritical*.tfstate[.backup], terraform.tfvars
Kubeconfigcritical*kubeconfig*, .kube/config
VCS directoryhigh.git/, .svn/, .hg/
Confighighconfig.yml/.json, settings.py, database.yml, wp-config.php, .npmrc, .dockercfg, appsettings*.json
Archivehigh*.zip/.tar/.tar.gz/.tgz/.7z/.rar
PIIhighemployee, customer, passport, aadhar/aadhaar, ssn, salary, payroll, kyc (substring match)
Logsmedium*.log, access[-_.]log, error[-_.]log, audit[-_.]log

Lead the finding title with the highest tier present (e.g. "publicly listable — credentials exposed") — that one line is what turns a generic bucket finding into something the client acts on immediately.

6.4 Dangling-CNAME bucket takeover — a bonus lead, always in scope

A candidate that returns 404/NoSuchBucket but is still the live target of a subdomain's CNAME is a claimable takeover, independent of the ownership-gating table above — the live CNAME is the ownership evidence (a stranger's dangling bucket doesn't happen to have the target's subdomain pointed at it). Detection only: never register the bucket to prove it. Category TAKEOVER, HIGH, CONFIRMED — the 404-plus-dangling-CNAME combination is unambiguous.

6.5 Recipes

Probe (bash):

NAME="acme-backup"
for url in \
  "https://${NAME}.s3.amazonaws.com/" \
  "https://storage.googleapis.com/${NAME}" \
  "https://${NAME}.blob.core.windows.net/?comp=list"; do
  echo "== $url =="
  curl -sk -m 8 -o /tmp/body.xml -w 'status=%{http_code}\n' "$url"
  grep -oE '<(ListBucketResult|EnumerationResults)|<Key>[^<]+</Key>|<Code>[A-Za-z]+</Code>' /tmp/body.xml | head -20
done

Probe (PowerShell):

$Name = "acme-backup"
$Targets = @(
  "https://$Name.s3.amazonaws.com/",
  "https://storage.googleapis.com/$Name",
  "https://$Name.blob.core.windows.net/?comp=list"
)
foreach ($u in $Targets) {
  try {
    $r = Invoke-WebRequest -Uri $u -TimeoutSec 8 -SkipHttpErrorCheck   # -SkipHttpErrorCheck is PS7+; on PS 5.1 read $_.Exception.Response in the catch
    "{0,-70} status={1}" -f $u, $r.StatusCode
    if ($r.Content -match '<ListBucketResult|<EnumerationResults') {
      [regex]::Matches($r.Content, '<Key>([^<]+)</Key>|<Name>([^<]+)</Name>') |
        Select-Object -First 15 |
        ForEach-Object { $_.Groups[1].Value + $_.Groups[2].Value }
    }
  } catch { "$u -> error: $($_.Exception.Message)" }
}

Candidate generation — apex + full prefix/suffix expansion (bash):

APEX="acme"
PREFIXES=("" "backup-" "assets-" "static-" "dev-" "prod-")
SUFFIXES=("" "-backup" "-assets" "-static" "-media" "-data" "-uploads" "-dev" "-prod" "-staging" "-logs" "-private" "-public" "-dump" "-archive")
for p in "${PREFIXES[@]}"; do
  for s in "${SUFFIXES[@]}"; do
    echo "${p}${APEX}${s}"
  done
done

Candidate generation (PowerShell):

$Apex = "acme"
$Prefixes = @("", "backup-", "assets-", "static-", "dev-", "prod-")
$Suffixes = @("", "-backup", "-assets", "-static", "-media", "-data", "-uploads", "-dev", "-prod", "-staging", "-logs", "-private", "-public", "-dump", "-archive")
foreach ($p in $Prefixes) { foreach ($s in $Suffixes) { "$p$Apex$s" } }

7. AWS Account-ID Recovery from a Leaked Access Key (Offline Decode)

Every AWS access key ID has the 12-digit owning account number encoded inside the key string itself — recoverable by a deterministic base32 decode with no secret key, no network call, and no live AWS credential. A single leaked AKIA…/ASIA…/AROA… string already in hand — even one that's long dead or rotated — discloses the org's AWS account number. This is genuinely offline OSINT (no authorization concern beyond already lawfully holding the key, §1), and it's a real gap in the general arsenal: offensive-osint §22.7 covers ARN-regex extraction and accountId JSON-field scraping, but not this decode.

7.1 Why the account ID matters

  • Cross-account trust-policy abuse — target trust policies that reference the account by number.
  • IAM role/principal enumeration — an sts:AssumeRole error-message differential ("not authorized" vs. "does not exist") distinguishes real principals from guesses. This reverse step needs the operator's own AWS credentials against the target account and is intrusive/CloudTrail-logged on the target's side — out of scope for this skill, described here only.
  • Targeted IAM-role phishing — a real account number lends legitimacy to a role-assumption pretext.

7.2 Key prefixes → entity type

The decode is identical across every prefix; the prefix only tells you which IAM entity type the key belongs to.

PrefixEntity
AKIAIAM user long-term access key
ASIATemporary (STS) credentials
AROAIAM role
AIDAIAM user
AGPAIAM user group
AIPAEC2 instance profile
ANPAManaged policy
ANVAManaged policy version
ABIAAWS STS service bearer token
ACCAContext-specific credential

Shape: 4-character prefix + exactly 16 base32 characters (A–Z, 2–7) = 20 characters total.

7.3 The algorithm

  1. Uppercase the string; take the first 20 characters.
  2. Confirm the first 4 characters are a valid prefix (§7.2) and the remaining 16 match [A-Z2-7]{16}.
  3. Base32-decode those 16 characters → 10 raw bytes.
  4. Take the first 6 bytes, interpret as a big-endian integer z.
  5. account = (z & 0x7FFFFFFFFF80) >> 7
  6. Zero-pad to 12 digits. If the result exceeds 999,999,999,999, the input wasn't really a key (reject).

Published by Aidan Steele / Tenable; reference implementation credit to A. Frichetten (hackingthe.cloud).

7.4 Runnable — stdlib-only Python

#!/usr/bin/env python3
"""Offline AWS account-ID decode. No secret, no network, no live key needed.
Mirrors core/aws_account.py's account_id_from_access_key(). Stdlib only.

Usage:
  echo "ASIAY34FZKBOKMUTVV7A" | python3 aws_account_decode.py
  python3 aws_account_decode.py AKIA... ASIA...
  python3 aws_account_decode.py < file_with_keys.txt
"""
import base64
import re
import sys

_VALID_PREFIXES = {"AKIA", "ASIA", "AROA", "AIDA", "AGPA", "AIPA",
                    "ANPA", "ANVA", "ABIA", "ACCA"}
_KEY_RE = re.compile(r"\b(A[A-Z0-9]{3}[A-Z2-7]{16})\b")
_MASK = 0x7FFFFFFFFF80
_MAX_ACCOUNT = 999_999_999_999

# AWS documentation / reserved example account IDs — never report these as a
# real disclosure; they pervade IAM policy docs, Terraform tutorials, SDK samples.
EXAMPLE_ACCOUNT_IDS = {
    "123456789012", "111122223333", "222233334444", "333344445555",
    "444455556666", "555566667777", "666677778888", "777788889999",
    "888899990000", "999900001111", "012345678901", "000000000000",
    "123412341234", "101010101010",
}


def account_id_from_access_key(key: str):
    k = (key or "").strip().upper()[:20]
    if len(k) != 20 or k[:4] not in _VALID_PREFIXES:
        return None
    body = k[4:]
    if not re.fullmatch(r"[A-Z2-7]{16}", body):
        return None
    try:
        decoded = base64.b32decode(body)
    except Exception:
        return None
    z = int.from_bytes(decoded[:6], "big")
    account = (z & _MASK) >> 7
    if account > _MAX_ACCOUNT:
        return None
    return f"{account:012d}"


def main() -> int:
    text = " ".join(sys.argv[1:]) or sys.stdin.read()
    seen = set()
    for m in _KEY_RE.finditer(text):
        key = m.group(1)
        if key in seen:
            continue
        seen.add(key)
        acct = account_id_from_access_key(key)
        if acct is None:
            continue
        flag = " (AWS EXAMPLE ID -- not a real disclosure)" if acct in EXAMPLE_ACCOUNT_IDS else ""
        print(f"{key} -> {acct}{flag}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

7.5 Test vector

ASIAY34FZKBOKMUTVV7A -> 609629065308

Verify any reimplementation against this before trusting its output.

7.6 Example-account screening

Screen every decoded (and every ARN-extracted) account ID against the 14 documented AWS example account IDs above — 123456789012 chief among them, present in nearly every AWS IAM policy doc, Terraform tutorial, and SDK sample. Skip this check and a plain documentation page in the response corpus mints a false "owned AWS account" finding.

Two complementary offline extraction paths, in addition to the decode:

  • ARN regex (any JSON/HTML/JS body): arn:aws[a-z0-9\-]*:[a-z0-9\-]*:[a-z0-9\-]*:(\d{12}):
  • AWS-qualified accountId field only — deliberately requires the aws qualifier (aws[_-]?account[_-]?id), never a bare "accountId":"…", since a bare field is just as likely an internal billing/customer record that happens to be 12 digits.

7.7 Ownership + severity/confidence

Decode every access key found in the corpus, plus pull account IDs from ARNs and AWS-qualified accountId fields. Ownership is structural, not scored:

Disclosure pathOwnershipSeverityConfidence
Key/ARN found on an in-scope host (the subdomain equals the target, or is a subdomain of it)OwnedMEDIUMFIRM
Key/ARN found on an out-of-scope/third-party host that merely references the accountReferencedINFOFIRM
Either of the above, additionally corroborated by a live STS-validated key (read-only GetCallerIdentity, offensive-osint §23.2)OwnedMEDIUMCONFIRMED

Every leaking page emits a DISCLOSES_ACCOUNT edge to the account it references — for owned and referenced accounts alike. An owned account additionally earns an OWNED_BY-target edge — a legitimate attack-path pivot. A referenced-only account has just the DISCLOSES_ACCOUNT edge and is never treated as the org's own escalation target — it's a third party's account, though still worth recording.

Note the deliberate severity ceiling: MEDIUM even for an owned account, because the finding itself is "your account number is now public" — real but modest on its own (rotate the leaked key; the number can't be un-disclosed). Severity escalates only through what an attacker does with the number next (§7.1), which this skill does not perform.


8. Dependency Confusion — Internal Signal + Public-Registry Unclaimed Confirmation

offensive-osint §44 covers registry search and secret hunting inside packages the target published. This is the opposite direction: what does the target's own manifests reference? A dependency name that carries an internal signal and is unclaimed on the public registry is a name an attacker can register today and have pulled into the target's build the next time a resolver falls back to the public index.

8.1 Manifest mining (read the target's own captured corpus — never re-fetch for this)

FileWhat it yields
package.json (dependencies/devDependencies/peerDependencies/optionalDependencies)npm package names
package-lock.json (v1 nested dependencies; v2/v3 packages keyed by node_modules/…)npm package names — richest source, includes transitive/scoped deps
.npmrc{scope}:registry= bindings + a bare default registry= — the private-registry signal
requirements.txtPyPI names + --index-url/--extra-index-url; screens out -e/local-path/git+/direct-URL deps (never a confusion vector)
pyproject.tomlPEP 621 project.dependencies + Poetry tool.poetry.dependencies/group.*.dependencies, plus [[tool.poetry.source]] private-index URLs; a Poetry dep carrying an explicit git/url/path/file key is direct-source and screened out
pip.conf / pip.iniindex-url/extra-index-url → private pip index signal
Any .js/.mjs/.cjs/.map bodyscoped npm specifiers (@scope/name) referenced in bundled/sourcemap code

8.2 The internal-signal classifier (zero I/O — pure text logic)

A dependency name is a candidate only when it carries one of these signals:

EcosystemSignalStrength
npm, scoped (@scope/pkg)the scope is bound to a private registry in .npmrcstrong
npm, scoped or unscoped.npmrc sets a private default registrystrong
npm, scoped or unscopedpackage name/scope matches an org-namespace token (≥4 chars, not a generic corporate word — corp, solutions, technologies, holdings, …)medium
PyPIa private index (--index-url / Poetry source / pip.conf) is configured anywhere in the corpusstrong
PyPIpackage name matches an org-namespace tokenmedium

Org-namespace tokens come from the target's registrable brand label — never the leftmost DNS label; a target reached via staging.acme.com yields the token acme, never staging — plus any ORG-typed asset already in the graph. A bare unscoped name with no private-registry binding and no namespace match carries no signal — indistinguishable from a typo or a yanked release — and is dropped, never emitted as a candidate.

8.3 The two-part confirmation contract — neither half alone is a finding

An internal signal alone is not proof the name is available — plenty of internal packages are also correctly published privately-but-globally-unique. A public-registry 404 alone is meaningless — a typo, an unpublished/yanked version, or a local-only dep all 404 too. Both must hold:

  1. Internal signal (§8.2) — screens out ordinary public dependencies and unknown bare names.
  2. Public-registry existence check returns 404 — a read-only GET against the real registry.npmjs.org / pypi.org, never the target. This is exactly why it's in scope under the pack's "public-registry 404 checks" line even though it's a live network call: the traffic lands entirely on a third-party public registry, zero packets to the client.

Fail closed on any ambiguity — a 200 (published), 403, 5xx, or timeout never emits a finding. Uncertainty is not evidence of claimability.

8.4 npm scope claimability — the nuance a bare-package check misses

For a scoped name (@scope/pkg), the package 404ing isn't the interesting fact — the whole scope must be unclaimed for the name to be squattable. Check the scope's org and user profile pages, not the public search API:

GET https://www.npmjs.com/org/{scope}    -> 200 = scope owned as an org
GET https://www.npmjs.com/~{scope}       -> 200 = scope owned as a user

The name is registerable only if both 404. The npm search API is deliberately not used for this: it returns total:0 for a scope that is privately owned but hosts no public packages, which would falsely read as "unclaimed." The org/user profile pages return a real 404 only when the scope genuinely doesn't exist, regardless of what it might privately host.

8.5 Severity / confidence

Signal strengthSeverityConfidence
strong (private-registry binding)HIGHFIRM
medium (org-namespace match only)MEDIUMTENTATIVE — namespace matching alone is namesake-prone

Never CONFIRMED — this is static manifest analysis over a captured corpus, not a live resolver-fallback observation. There is no "the build actually pulled the malicious package" event to point at.

8.6 Recipes

bash:

# unscoped npm
curl -sk -m 15 -o /dev/null -w '%{http_code}\n' "https://registry.npmjs.org/acme-internal-utils"
# scoped npm -- package + both scope-ownership pages
curl -sk -m 15 -o /dev/null -w '%{http_code}\n' "https://registry.npmjs.org/@acme%2Fdesign-system"
curl -sk -m 15 -o /dev/null -w '%{http_code}\n' "https://www.npmjs.com/org/acme"
curl -sk -m 15 -o /dev/null -w '%{http_code}\n' "https://www.npmjs.com/~acme"
# PyPI
curl -sk -m 15 -o /dev/null -w '%{http_code}\n' "https://pypi.org/pypi/acme-internal-tools/json"

PowerShell:

function Test-RegistryStatus {
  param([string]$Url)
  try {
    $r = Invoke-WebRequest -Uri $Url -Method Get -TimeoutSec 15 -SkipHttpErrorCheck   # PS7+; on PS 5.1 read $_.Exception.Response in the catch
    return $r.StatusCode
  } catch { return "error" }
}
Test-RegistryStatus "https://registry.npmjs.org/acme-internal-utils"
Test-RegistryStatus "https://www.npmjs.com/org/acme"
Test-RegistryStatus "https://www.npmjs.com/~acme"
Test-RegistryStatus "https://pypi.org/pypi/acme-internal-tools/json"

Bound the check volume — filter to internal-signalled candidates before hitting the registry (hundreds, not thousands, per engagement) and cache scope-ownership lookups (one org/user check per distinct scope, never per package under that scope).

8.7 Out of scope

Reserving the name — publishing a stub the org controls — is a remediation the client performs, not a step this skill takes. This skill's ceiling is: "this name is internal-signalled and currently unclaimed." Confirming that a live resolver fallback actually pulls a planted package, or registering anything under the discovered name, is active supply-chain interference and out of scope regardless of authorization — hand it to the client as the remediation action (§8.5 evidence + §10 business translation).


9. Cloud-Native & Container/K8s/CI Control-Plane Fingerprinting — an Org-Attribution Surface

Modern infrastructure increasingly is the org-attribution signal: a Lambda Function URL, a *.run.app Cloud Run service, or an exposed kubelet is both an exposure and a fingerprint of which cloud account/cluster belongs to the target. Everything in this section is passive — pattern-matching over hostnames and ports already resolved by earlier recon, zero new network calls of this skill's own. offensive-osint §16.17–§16.19 carries the fuller URL-pattern and active-probe tables (20 cloud-native providers, the full container-registry search list, 11 CI/CD platforms with active curl recipes); this section is the reasoning layer that decides what a hit is worth and whether it's the target's.

9.1 Cloud-native endpoint fingerprint

Match already-resolved hostnames — subdomain CNAME targets, certificate SANs, subdomain FQDNs themselves, and webapp URLs — against provider URL patterns:

ProviderPatternService
AWS*.execute-api.<region>.amazonaws.comAPI Gateway
AWS*.lambda-url.<region>.on.awsLambda Function URL
AWS*.<region>.elasticbeanstalk.comElastic Beanstalk
Azure*.azurewebsites.netApp Service
Azure*.azurecontainerapps.ioContainer Apps
Azure*.file|queue|table.core.windows.netStorage (Blob object storage is handled by bucket discovery, §6 — not duplicated here)
GCP*.run.appCloud Run
GCP*.appspot.comApp Engine
GCP*.cloudfunctions.netCloud Function

(offensive-osint §16.17 extends this to 20 providers — App Runner, CloudFront, ALB/ELB, Amplify, Static Web Apps, Vercel, Netlify, Cloudflare Workers/Pages, Heroku, Render, Fly.io, Railway, DigitalOcean App Platform.)

Ownership gating is structural, not scored — the same discipline as §6.3's bucket model, applied to endpoints:

Provenance of the matchOwnership
A subdomain CNAME points at the patternOwned
A certificate SAN names the patternOwned
The subdomain's own FQDN matches the pattern (it is the cloud-native host)Owned
A webapp URL matches the pattern with no independent tie to the target from the aboveUnverified

Owned → MEDIUM/FIRM. Unverified (a bare pattern match with no corroborating tie) → LOW/TENTATIVE. Confirming whether the endpoint is actually invocable without authentication is the stage-6 --validate --validate-cloud step — out of scope; this section only says "this cloud-native surface exists and appears to belong to the target," never "it's exploitable."

9.2 Container / Kubernetes control-plane exposure — passive flag, not active probe

Rather than issuing new probes, treat orchestration control-plane ports already discovered by port/service enumeration as a likely-exposed control plane. All five carry HIGH severity, FIRM confidence once flagged — the port being open is directly observed; the auth posture behind it is not, which is exactly why confirmation is gated separately:

PortServiceWhy it matters if unauthenticated
2375Docker API (plain HTTP, no TLS)Anyone who reaches it controls the daemon — launch a host-mounting container, escape to root on the host.
2376Docker API (TLS)Equivalent to 2375 unless client-cert auth is strictly enforced.
2379etcd client APIHolds the entire cluster state — every Secret, in plaintext. Read = full credential harvest; write = cluster tampering.
6443Kubernetes API serverThe cluster's control plane — a valid token, leaked kubeconfig, or anonymous-auth misconfig creates privileged pods and reads every Secret.
10250kubeletNo auth required = pod exec on the node.

(offensive-osint §16.18 has the fuller table — kube-proxy/controller-manager/scheduler health/metrics endpoints, cAdvisor, Helm Tiller — plus the active curl recipe for each and the public-container-registry search list across Docker Hub/Quay/GHCR/ECR Public.)

The port being open says nothing about its auth posture. Flag it; do not connect to it to find out. A live GET/HEAD against the endpoint to confirm whether it answers unauthenticated is exactly the line this skill stops at.

9.3 CI/CD platform surface

Fingerprint from tech strings already collected by passive web/HTTP enumeration — Jenkins, GitLab (self-hosted), Argo CD, Harbor, TeamCity, Drone. MEDIUM/TENTATIVE — a tech-string match is a weaker signal than a resolved hostname pattern, hence TENTATIVE rather than FIRM. A CI/CD console or API, when reachable unauthenticated, exposes build pipelines, stored credentials, and artifact registries — a supply-chain foothold. offensive-osint §16.19 has the per-platform active probe paths (/script, /api/v4/version, /gate/info, …) and the GitHub Actions secret-leak anti-pattern catalog (workflows that echo ${{ secrets.* }} to logs, or check out fork-PR code under pull_request_target) for an operator who chooses to go further under their own authority.

9.4 What's out of scope here

Any GET/HEAD issued specifically to confirm a fingerprinted cloud-native endpoint or control-plane port is reachable/unauthenticated is a stage-6 --validate --validate-cloud active tier — hard-gated behind explicit --validate plus per-target scope confirmation, default OFF. This skill's ceiling is the passive fingerprint and the "likely exposed, auth posture unconfirmed" flag; describe the confirmation step, never perform it.


10. Severity & Business Translation

Technical findingBusiness languageSeverity
Publicly listable bucket, ownership-verified, credentials/db-dump objects presentCustomer/internal data — including live credentials — is downloadable by anyone on the internet right now.CRITICAL
Same listable bucket, but the name can't be tied to the clientSomeone's bucket is open; not provably this client's exposure. Logged for the graph, not reported as their risk.No finding (§6.3)
AWS account ID recovered from a dead/rotated key, owned hostThe org's cloud account number is now public. Not itself a breach, but every cross-account trust policy referencing that number needs review.MEDIUM
Dependency-confusion vector, strong signal (private-registry binding), unclaimed on npmAn attacker can publish a package under this exact internal name today; the next CI run with a misconfigured registry fallback pulls it and runs arbitrary code in the build.HIGH
Cloud-native endpoint (Lambda URL / Cloud Run), owned, auth posture unconfirmedA managed-service endpoint is reachable; whether it's invocable without authentication is not yet proven — an open question requiring confirmation, not an assumed breach.MEDIUM
Kubernetes API / etcd / kubelet port open, passively flaggedThe cluster's control plane appears internet-reachable; authentication enforcement is unconfirmed. If it isn't enforced, this is a path to full cluster compromise.HIGH (unconfirmed auth)

11. Skill Self-Test

Drop these into a fresh session to verify the skill loads and routes correctly.

  1. "Generate S3/GCS/Azure bucket candidates for acme.com with subdomains api/billing/hr and probe them." → §6.1–§6.2, §6.5.
  2. "We found a listable S3 bucket named marketplace-media from a permutation guess — can't tie it to the client. Is that CRITICAL?" → negative. §6.3 — no; an unverified public/listable hit produces no finding at all (not even INFO); it's recorded as an asset only.
  3. "Same bucket, but its object listing includes acme-employee-payroll.csv." → §6.3 branded-object-key ownership signal — now verified, promote to CRITICAL.
  4. "What's exposed inside a listable bucket, without downloading any object?" → §6.3 object-key triage table (filename-pattern classification of the listing body only).
  5. "A subdomain has a live CNAME to a bucket that returns 404 NoSuchBucket. What's the finding?" → §6.4 — dangling-CNAME takeover, HIGH, CONFIRMED, independent of the ownership-gating table.
  6. "Decode the AWS account ID from ASIAY34FZKBOKMUTVV7A." → §7.3–§7.5, test vector 609629065308.
  7. "We found 123456789012 as an accountId in a Terraform snippet on the target's site. Real disclosure?" → §7.6 — no; that's a documented AWS example account ID, screen it out.
  8. "Should I use the decoded account ID to enumerate IAM roles via sts:AssumeRole?" → negative. §7.1/§5 — out of scope; needs the operator's own AWS credentials against the target account and is CloudTrail-logged; describe the pivot value, don't perform it.
  9. "package.json references @acme/design-system, and .npmrc binds @acme to a private registry. Is that a finding by itself?" → §8.2–§8.3 — internal signal only; still needs the public-registry 404 (and scope-claimability, §8.4) check before it's a finding.
  10. "npm's public search API returns total:0 for @acme. Is the scope claimable?" → §8.4 — no; the search API is blind to privately-owned scopes. Use the org/user profile-page 404 check instead.
  11. "Confirmed unclaimed dependency-confusion vector. Should we reserve the package name to protect the client?" → negative. §8.7 — out of scope; that's a client remediation action, not this skill's step.
  12. "A subdomain CNAMEs to *.run.app. Is that Cloud Run endpoint owned or unverified?" → §9.1 — CNAME provenance = owned.
  13. "Port 6443 is open per a prior port scan. What do I do next?" → §9.2/§9.4 — flag as a likely-exposed Kubernetes API control plane (passive, HIGH/FIRM); do not curl /api to confirm auth posture — that's stage-6 --validate --validate-cloud.
  14. "Write the business-translation line for a HIGH-signal dependency-confusion finding." → §10.

12. Changelog

  • v1.0 (2026-08-06) — initial release. Reproduces exact logic from the reference implementation: modules/cloud_buckets.py (two-class candidate generation, HEAD/GET→listing probe technique, the ownership-gated severity model, 9-tier object-key triage, dangling-CNAME takeover), core/aws_account.py + modules/aws_account_enum.py (offline AKIA/ASIA/AROA→account-ID base32 decode, canonical test vector, AWS documentation-example-ID screening, ownership/severity/confidence mapping), core/dep_confusion.py + modules/dependency_confusion.py (manifest mining across 7 file types, the internal-signal classifier, the two-part confirmation contract, the npm scope-claimability nuance), and modules/cloud_exposure.py (cloud-native endpoint fingerprint with structural ownership gating, passive container/K8s/CI exposure flags). Deepens offensive-osint §16.8/§16.17–16.19/§44 rather than duplicating their wordlists.

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Turns one-shot external recon into a continuous monitoring program. Covers the scheduled re-scan-and-diff loop (baseline snapshot -> interval sleep -> re-scan -> asset/finding delta -> threshold-gated webhook alert), the scan-to-scan diff engine (new/removed/changed assets by a tracked-attribute table, new/resolved findings by a stable cross-scan fingerprint), adversary CTI / chatter monitoring across six public feeds (ransomwatch, ransomware.live, HackerNews Algolia search, Reddit security-subreddit RSS, GitHub Gist code-search, public Telegram channel scraping) with a source-kind-aware severity engine (leak-site/forum/telegram/paste tiers, CRITICAL through INFO), literal/glob/regex watchlist pattern matching, full-corpus capture with retroactive rescan on new watchlist entries, infrastructure-tracking-over-time discipline (certificate-transparency, passive-DNS, port/service, and typosquat re-enumeration cadence, and what a genuine 'perimeter drift' event looks like in the diff output), a five-state finding-lifecycle state machine (open/triaged/risk_accepted/resolved/false_positive) with per-severity SLA and fingerprint-based cross-scan dedup and auto-resolve/reopen rules, the alert-fatigue trap where a lifecycle-unaware rule re-fires on an already-accepted finding, a durable retry/backoff alert-outbox pattern ('queued is not delivered'), and copy-paste bash-cron plus PowerShell-Scheduled-Task recipes for a re-scan+diff loop with the Slack-compatible webhook payload shape. Passive OSINT and analysis only -- no new active-intrusion technique. Use when setting up ongoing monitoring for a retainer or MSSP engagement, tuning alert thresholds to avoid fatigue, triaging a finding's lifecycle status, investigating adversary chatter about a brand, building a 'what changed on the perimeter since last week' report, or deciding whether a persisting finding should re-alert.

日本語の概要は準備中です。原文の説明を表示しています。

elementalsouls/Claude-OSINT2,8042026年10月10日 更新

Rigorous, defensible email-spoofability verdict and SPF supply-chain risk analysis computed from published DNS alone. Deepens the record-level SPF/DMARC/DKIM/BIMI/MTA-STS/DNSSEC fetch recipes in the offensive-osint arsenal (§16.14) with the reasoning that section doesn't do: a priority-ordered composite verdict for whether an attacker can actually land header-From-spoofed mail in an inbox, and by which vector (exact-domain vs subdomain) — grounded in the single most-misunderstood distinction in email security: the envelope MAIL FROM that SPF authenticates vs the visible header From: that only DMARC governs. Explains precisely why SPF -all/~all alone is NOT spoof-proof without DMARC enforcement, and why SPF +all bypasses DMARC even under p=reject pct=100. Covers RFC 7208 §4.6.4's 10-DNS-lookup / 2-void-lookup PermError fail-open condition with a runnable stdlib-only lookup-counter script, plus the SPF-include-takeover supply-chain vector (an attacker re-registering a dead include inherits SPF-pass authority over the victim domain) with strict transient-vs-NXDOMAIN discrimination discipline so a temporary SERVFAIL is never mistaken for a takeover lead. Fully passive: DNS TXT reads only, no mail sent, no RCPT TO probe, no API keys. Use when auditing a domain's real spoofing resistance (not just its published records), explaining to a client why 'we have SPF -all' does not mean they're covered, investigating an SPF PermError or an unusually long include chain, evaluating a dead SPF include as a takeover lead, or writing a defensible spoofability finding for a deliverable.

日本語の概要は準備中です。原文の説明を表示しています。

elementalsouls/Claude-OSINT2,8042026年10月10日 更新

FAIR-aligned exposure quantification: turns a pile of recon findings into a defensible 0-100 + A-F org risk score (Likelihood x Impact, three ownership-aware factors: exposure/threat/impact), an ownership + proof demotion cap so unproven or weakly-owned findings can't inflate the number, a $-denominated FAIR loss-magnitude estimate (IBM/Ponemon per-record cost bands, cross-source record dedup, threat-factor annualization), attack-path amplification (curated red-team chain catalog + generic graph-walk engine, with a kill-chain vs shared-fate honesty gate), and a board-ready one-pager deliverable (hero $ + letter grade + top-3 findings + top attack path + the ask). Extends osint-methodology's severity rubric and client deliverable templates with quantification. Passive analysis only -- operates on findings already collected, no target traffic, no API keys. Use when asked to score risk, quantify exposure, estimate breach cost, build a board report, translate technical findings to dollars, or explain why a grade or dollar figure came out the way it did.

日本語の概要は準備中です。原文の説明を表示しています。

elementalsouls/Claude-OSINT2,8042026年10月10日 更新

Organization-grade identity-fabric mapping: tenant/federation fingerprinting and the pre-auth user-ENUMERATION oracle methodology — enumeration and fingerprint only, never credential submission. Covers domain-to-tenant resolution (Microsoft getuserrealm.srf Managed/Federated namespace check, Entra OIDC metadata tenant-GUID extraction, Autodiscover v2), keyless Microsoft tenant-federation mapping (GetFederationInformation SOAP -> sibling-domain discovery, discover-only ROE, FEDERATED_WITH provenance edge held out of attack-path pivoting), Okta org-slug derivation + OIDC fingerprint + governed custom-domain enumeration, ADFS passive/active fingerprint + version inference, Google Workspace MX-correlated detection, generic OIDC (Auth0/Keycloak/Ping Identity/OneLogin/Duo) discovery, SAML metadata (5 paths), Azure AD Seamless-SSO Negotiate-challenge detection, Microsoft Defender for Identity (MDI) sensor-API presence check, the user-enumeration oracle methodology for Microsoft GetCredentialType (IfExistsResult semantics: exists / doesn't-exist / exists-in-federated-tenant / throttled) and Okta /api/v1/authn (errorCode differential), Medium-detectability discipline with a hard 20-candidate-per-tenant cap and admin/role interest-based ranking, and name x confirmed-email-pattern login-candidate synthesis that FAILS CLOSED with zero output when no org pattern is confirmed. Grounded directly in a production ASM implementation's sso_idp.py, tenant_recon.py, and core/email_patterns.py modules. Deepens — does not duplicate — offensive-osint skill's Identity Fabric endpoint reference with the tenant-federation MAP, the oracle WORKFLOW, and the candidate-SYNTHESIS methodology that reference lacks. Use when fingerprinting an organization's identity provider, mapping its tenant/federation boundary, running an authorized pre-auth user-enumeration pass, or synthesizing login candidates from harvested names to feed that oracle — never for password spray, credential submission, or auth bypass.

日本語の概要は準備中です。原文の説明を表示しています。

elementalsouls/Claude-OSINT2,8042026年10月10日 更新

Org-grade attack-surface mapping: given a company's legal identity, discover its ENTIRE owned internet footprint — corporate family -> owned domains -> owned netblocks/ASN -> live assets — with attribution discipline, not just DNS breadth. The org-first attribution pyramid (legal entity -> LEI/registration -> corporate family -> owned domains -> owned netblocks/ASN -> live assets). Corporate-identity resolution via the GLEIF LEI API (legal name -> LEI, exact-LEI direct-children expansion, downward-only depth-capped BFS, NEVER a name re-resolution), SEC-EDGAR full-text search + Exhibit-21 subsidiary entity names, OpenCorporates entity corroboration, Wikidata SPARQL corporate graph (P856/P355/P749/P1830). Domain attribution via reverse-WHOIS (WhoisXML preview-then-purchase quota guard, SecurityTrails associated-domains — both paid), crt.sh O= certificate-transparency organization pivot (keyless), infrastructure correlation (shared NS/MX/SaaS-TXT, netblock membership, reverse-DNS PTR), and the independent-evidence combiner (1-prod(1-w_i), rule of three, OwnerTier NONE/WEAK/MODERATE/STRONG/CONFIRMED) — discover-only `related:` candidates are NEVER auto-scanned. Netblock/ASN attribution via org-first RIR queries (ARIN Whois-RWS org-handle search, RIPE DB organisation + inverse-org search) that recover 'dark netblocks' with no DNS link to the seed, ASN discovery (RIPEstat + BGPView union), the HYPERSCALER-SCOPE GUARD (never attribute a whole AWS/GCP/Azure/Cloudflare announced range to a tenant — keep only the seed-containing block, tag shared_hosting_cdn), and org-identity-seeded internet-scan-index queries (Shodan/Censys/ZoomEye/FOFA/BinaryEdge org: filters + an always-on keyless crt.sh fallback). Promote-to-scan triage ranks forgotten discover-only netblocks by remote-access exposure (gateway-vendor/KEV/control-plane/datastore port scoring) into an operator queue. Attribution confidence rubric + anti-patterns (namesake grafting via GLEIF name re-resolution, single-signal ownership, hyperscaler over-attribution, privacy-WHOIS pivot poisoning, RIR org-name collisions). Passive/keyless-first OSINT only — every paid-key dependency is enrichment on top of a keyless core, never a hard requirement. Use when mapping an organization's full corporate-family internet footprint, resolving a legal entity to its LEI/subsidiaries, discovering domains/netblocks/ASNs an org owns beyond its one seed domain, auditing M&A/shadow-IT sprawl, or scoping an engagement that starts from a company NAME rather than a domain.

日本語の概要は準備中です。原文の説明を表示しています。

elementalsouls/Claude-OSINT2,8042026年10月10日 更新

End-to-end external OSINT engagement autopilot. Run the FULL osint-methodology pipeline to completion in ONE go for an authorized domain — engagement folder, Stages 1-5 (seed, expansion, enrichment, exposure, convergence), multi-agent per-host content+JS fan-out, headline verification, auto-generated findings, and a consolidated multi-tab .xlsx deliverable. Stops ONLY for Stage 6 (active exploitation) arming and out-of-scope sibling assets. Use whenever asked to "run OSINT / recon / attack-surface" on a target — do NOT deliver a thin passive first pass.

日本語の概要は準備中です。原文の説明を表示しています。

elementalsouls/Claude-OSINT2,8042026年10月10日 更新

elementalsouls のスキルをすべて見る

このスキルの問題を報告する