provider capability model, on-demand scan/backfill, more providers
Readability capabilities (exists/object_readable/listable) are now tracked
separately instead of a single exposure flag, matching how providers differ:
- Azure 'Blob' mode = object readable, not listable; 'Container' = listable
- Cloudflare R2 public URLs are never anonymously listable (root 500); R2 is
object-read only and is no longer probed for listing
- S3/GCS: 200 list = listable; 403 + readable index.html/robots.txt =
object readable only
New columns: cloud_assets.listable, cloud_assets.object_readable.
Azure: account-level container listing is never anonymous; discover containers
by probing a bounded common-name set (or the container from a URL path), then
list blobs.
Classifier: added the legacy AWS '.s3-<region>.amazonaws.com' bucket form.
On-demand scan: POST /api/scan {target} classifies a host or URL, follows CNAME
for custom domains, probes, and indexes readable resources; Azure container
comes from the URL path. UI has a Scan box.
Backfill: POST /api/reprobe re-probes already-discovered assets with the current
checks and indexes any readable ones, recovering findings from earlier probes.
DNS: CNAME-only enrichment (A/AAAA resolved only for matches), 64 workers,
larger queue.
Verified live against real public resources:
veteran1.nyc3.digitaloceanspaces.com listable, entries
bucket-01.s3-eu-west-1.amazonaws.com listable, entries
tartugeo-podcast.storage.googleapis.com listable, entries
eroticfeeldev.blob.core.windows.net/images listable, entries
This commit is contained in:
1 parent
a366905a43
commit
62a7d2eb2e
23 files changed
+936
-165
No files matched your search
+53
-5
@@ -143,6 +143,50 @@ func nonNil(s []string) []string {
|
||||
return s
|
||||
}
|
||||
|
||||
// ListProbeTargets returns persisted assets to re-probe (for backfill). An
|
||||
// empty exposure matches all.
|
||||
func (s *Store) ListProbeTargets(ctx context.Context, exposure string, limit int) ([]model.ProbeTarget, error) {
|
||||
if limit <= 0 || limit > 100000 {
|
||||
limit = 10000
|
||||
}
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT provider, service, provider_hostname, resource_identifier, region
|
||||
FROM cloud_assets
|
||||
WHERE ($1 = '' OR exposure_state = $1)
|
||||
ORDER BY last_seen DESC
|
||||
LIMIT $2`, exposure, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := []model.ProbeTarget{}
|
||||
for rows.Next() {
|
||||
var t model.ProbeTarget
|
||||
if err := rows.Scan(&t.Provider, &t.Service, &t.Hostname, &t.Resource, &t.Region); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, t)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// EnsureAsset upserts a manually scanned asset (which may not have appeared in
|
||||
// CT) and returns its id.
|
||||
func (s *Store) EnsureAsset(ctx context.Context, c model.Candidate) (int64, error) {
|
||||
var id int64
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
INSERT INTO cloud_assets
|
||||
(provider, service, hostname, provider_hostname, resource_identifier,
|
||||
region, resource_specific, public_semantic, first_seen, last_seen)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, now(), now())
|
||||
ON CONFLICT (provider, service, provider_hostname)
|
||||
DO UPDATE SET last_seen = now()
|
||||
RETURNING id`,
|
||||
c.Provider.String(), c.Service.String(), c.Original, c.Hostname, c.Resource,
|
||||
c.Region, c.ResourceSpecific, c.PublicSemantic.String()).Scan(&id)
|
||||
return id, err
|
||||
}
|
||||
|
||||
// IndexEntry is one indexed resource item for the UI.
|
||||
type IndexEntry struct {
|
||||
ID int64 `json:"id"`
|
||||
@@ -267,12 +311,13 @@ func (s *Store) CountAssets(ctx context.Context, f AssetFilter) (int, error) {
|
||||
}
|
||||
|
||||
// UpdateExposure records the outcome of an optional reachability probe.
|
||||
func (s *Store) UpdateExposure(ctx context.Context, provider, service, providerHostname string, state model.ExposureState, detail string) error {
|
||||
func (s *Store) UpdateExposure(ctx context.Context, provider, service, providerHostname string, state model.ExposureState, detail string, listable, objectReadable bool) error {
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
UPDATE cloud_assets
|
||||
SET exposure_state = $4, exposure_detail = $5, probed_at = now()
|
||||
SET exposure_state = $4, exposure_detail = $5, probed_at = now(),
|
||||
listable = $6, object_readable = $7
|
||||
WHERE provider = $1 AND service = $2 AND provider_hostname = $3`,
|
||||
provider, service, providerHostname, state.String(), detail)
|
||||
provider, service, providerHostname, state.String(), detail, listable, objectReadable)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -288,6 +333,8 @@ type Asset struct {
|
||||
PublicSemantic string `json:"public_semantic"`
|
||||
Exposure string `json:"exposure_state"`
|
||||
ExposureDetail string `json:"exposure_detail"`
|
||||
Listable bool `json:"listable"`
|
||||
ObjectReadable bool `json:"object_readable"`
|
||||
FirstSeen time.Time `json:"first_seen"`
|
||||
LastSeen time.Time `json:"last_seen"`
|
||||
ProbedAt *time.Time `json:"probed_at"`
|
||||
@@ -327,7 +374,8 @@ func (s *Store) ListAssets(ctx context.Context, f AssetFilter) ([]Asset, error)
|
||||
}
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, provider, service, hostname, provider_hostname, resource_identifier,
|
||||
region, public_semantic, exposure_state, exposure_detail, first_seen, last_seen, probed_at
|
||||
region, public_semantic, exposure_state, exposure_detail, listable, object_readable,
|
||||
first_seen, last_seen, probed_at
|
||||
FROM cloud_assets
|
||||
WHERE ($1 = '' OR provider = $1)
|
||||
AND ($2 = '' OR service = $2)
|
||||
@@ -345,7 +393,7 @@ func (s *Store) ListAssets(ctx context.Context, f AssetFilter) ([]Asset, error)
|
||||
var a Asset
|
||||
if err := rows.Scan(&a.ID, &a.Provider, &a.Service, &a.Hostname, &a.ProviderHost,
|
||||
&a.Resource, &a.Region, &a.PublicSemantic, &a.Exposure, &a.ExposureDetail,
|
||||
&a.FirstSeen, &a.LastSeen, &a.ProbedAt); err != nil {
|
||||
&a.Listable, &a.ObjectReadable, &a.FirstSeen, &a.LastSeen, &a.ProbedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, a)
|
||||
|
||||
Reference in new issue
Block a user