Skip to content

Commit 2f36993

Browse files
h0tak88rclaude
andcommitted
fix(program-monitor): Intigriti handle parser flood — stop endless Discord alerts
Every Intigriti program ended up with Handle="detail" because fetchITPrograms parsed webLinks.detail (".../programs/<company>/<handle>/detail") with a plain LastIndex("/") and grabbed the literal trailing path segment. So programScopeCacheKey("it", "detail") returned "it:detail" for ALL IT programs — they shared one program_assets row, each sweep overwrote the prior program's assets, and the next program's full asset set read as "new". Endless alert flood (NVIDIA: 11 new, Salto: 14 new, repeat). Fixes: - programs_api.go fetchITPrograms: strip a trailing "/detail" before extracting the last segment. Unit-tested: nvidiapublicbugbounty/detail -> "nvidiapublicbugbounty" (was "detail"), trailing-slash + legacy "=" form both pass. - program_monitor.go: programMonitorKey(p) prefers platform:id:<UUID> when p.ID is set (defense in depth — any future handle-parsing bug can't collide programs). - program_monitor.go: safety cap — if newAssets >= 20 OR equals the full asset set, log a warning and silently re-baseline instead of alerting. Real scope additions are 1-5 assets at a time; a huge delta almost always means a key collision. - app.go boot: one-shot cleanup deletes program_assets WHERE program_key='it:detail' (the poisoned bucket). H1/BC programs re-baseline silently under their new ID keys. Reviewed: no runtime bugs across parser correctness, key-transition, cap, cleanup safety, DB impls, concurrency. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 3cc68a8 commit 2f36993

7 files changed

Lines changed: 93 additions & 4 deletions

File tree

internal/api/program_monitor.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,25 @@ func collectProgramsForMonitor() []ProgramSummary {
140140
return all
141141
}
142142

143+
// programMonitorKey returns a unique-per-program key for the program_assets store.
144+
// p.ID (the platform's stable UUID/ID) is preferred when present so a future handle
145+
// parsing bug can't collapse multiple programs into one bucket — that exact mistake
146+
// is what caused the Intigriti "/detail" flood: every IT program ended up with
147+
// Handle="detail" and shared a single program_assets row.
148+
func programMonitorKey(p ProgramSummary) string {
149+
id := strings.TrimSpace(p.ID)
150+
if id != "" {
151+
return strings.ToLower(p.Platform) + ":id:" + id
152+
}
153+
return strings.ToLower(p.Platform) + ":" + p.Handle
154+
}
155+
156+
// floodSuspectThreshold caps a single program's "new asset" alert. Real scope
157+
// additions are 1-5 assets at a time; a huge "diff" on a watched catalogue is
158+
// almost always an identifier-collision/key bug. Above this we silently re-baseline
159+
// and log a warning instead of flooding Discord.
160+
const floodSuspectThreshold = 20
161+
143162
// sweepProgram fetches one program's current in-scope assets and alerts on new ones.
144163
func sweepProgram(p ProgramSummary) {
145164
assets := fetchProgramAssets(p)
@@ -148,7 +167,7 @@ func sweepProgram(p ProgramSummary) {
148167
// otherwise the next successful fetch would report every asset as "new".
149168
return
150169
}
151-
key := programScopeCacheKey(p.Platform, p.Handle)
170+
key := programMonitorKey(p)
152171
newAssets, firstRun, err := db.RecordProgramScopeAssets(key, assets)
153172
if err != nil {
154173
logger.GetLogger().Infof("[PROGRAM-MONITOR] record failed for %s: %v", key, err)
@@ -157,6 +176,14 @@ func sweepProgram(p ProgramSummary) {
157176
if firstRun || len(newAssets) == 0 {
158177
return // baseline run, or nothing new
159178
}
179+
// Safety cap: a real scope change is incremental. A huge sudden delta almost
180+
// always means a key collision (multiple programs mapped to one row) or a
181+
// platform API change. Skip the alert rather than spam.
182+
if len(newAssets) >= floodSuspectThreshold || len(newAssets) == len(assets) {
183+
logger.GetLogger().Infof("[PROGRAM-MONITOR] WARN suspicious delta for %s (%s): %d/%d assets reported new — silently re-baselining, no alert sent",
184+
p.Handle, p.Platform, len(newAssets), len(assets))
185+
return
186+
}
160187
alertNewAssets(p, newAssets)
161188
}
162189

internal/api/programs_api.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -821,9 +821,17 @@ func fetchITPrograms(bbpOnly, includeScope bool) ([]ProgramSummary, error) {
821821
if i := strings.Index(detail, "="); i >= 0 && i+1 < len(detail) {
822822
programPath = detail[i+1:]
823823
}
824-
handle := programPath
825-
if i := strings.LastIndex(strings.TrimRight(programPath, "/"), "/"); i >= 0 {
826-
handle = strings.TrimRight(programPath, "/")[i+1:]
824+
// Intigriti's webLinks.detail paths look like
825+
// ".../programs/<company>/<program-handle>/detail"
826+
// — the LAST segment is the literal word "detail", not the handle.
827+
// Strip a trailing "/detail" first, then take the last segment.
828+
// (Without this every IT program gets handle="detail", which collides
829+
// into a single program_assets bucket and floods the scope monitor.)
830+
handlePath := strings.TrimRight(programPath, "/")
831+
handlePath = strings.TrimSuffix(handlePath, "/detail")
832+
handle := handlePath
833+
if i := strings.LastIndex(handlePath, "/"); i >= 0 {
834+
handle = handlePath[i+1:]
827835
}
828836
name := strings.TrimSpace(rec.Get("name").Str)
829837
if name == "" {

internal/app/app.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ func StartAPI() error {
7373
// the persisted state is truthful and monitoring continues after a Docker restart.
7474
resumeMonitorsOnStartup()
7575

76+
// One-shot cleanup: a previous build mis-parsed Intigriti program handles as the
77+
// literal "detail" path segment, collapsing every IT program into a single
78+
// program_assets bucket and producing a Discord alert flood. Wipe that row so the
79+
// monitor re-baselines under the now-correct keys.
80+
cleanupCorruptedProgramAssets()
81+
7682
// Bug-bounty scope-change monitor: slow rolling sweep of all programs that alerts
7783
// to MONITOR_WEBHOOK_URL (Discord) whenever a new in-scope asset appears. No-op when
7884
// no webhook is configured or PROGRAM_MONITOR=off.
@@ -133,6 +139,21 @@ func reconcileStaleScansOnStartup() {
133139
}
134140
}
135141

142+
// cleanupCorruptedProgramAssets drops program_assets rows poisoned by past
143+
// identifier-collision bugs (currently: every Intigriti program shared the key
144+
// "it:detail" because the handle parser took "/detail" as the handle). Safe to call
145+
// every boot — it only deletes the known-bad keys; healthy rows are untouched.
146+
func cleanupCorruptedProgramAssets() {
147+
if err := db.Init(); err != nil {
148+
return
149+
}
150+
for _, key := range []string{"it:detail"} {
151+
if n, err := db.DeleteProgramScopeAssetsByKey(key); err == nil && n > 0 {
152+
log.Printf("[INFO] Cleaned %d poisoned program_assets row(s) under key %q.", n, key)
153+
}
154+
}
155+
}
156+
136157
// resumeMonitorsOnStartup restarts the monitor daemon goroutines (which don't survive
137158
// a process restart) when the DB still has monitor targets marked is_running, so polling
138159
// actually resumes instead of the dashboard merely showing a stale "running" flag.

internal/db/db.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,16 @@ func RecordProgramScopeAssets(programKey string, assets []string) ([]string, boo
162162
return dbInstance.RecordProgramScopeAssets(programKey, assets)
163163
}
164164

165+
// DeleteProgramScopeAssetsByKey clears stored assets for a program key.
166+
func DeleteProgramScopeAssetsByKey(programKey string) (int64, error) {
167+
if dbInstance == nil {
168+
if err := Init(); err != nil {
169+
return 0, err
170+
}
171+
}
172+
return dbInstance.DeleteProgramScopeAssetsByKey(programKey)
173+
}
174+
165175
// InsertKeyhackTemplate inserts or updates a KeyHack template
166176
func InsertKeyhackTemplate(keyname, commandTemplate, method, url, header, body, notes, description string) error {
167177
if dbInstance == nil {

internal/db/postgres.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,15 @@ func (p *PostgresDB) ListProgramScopeAssets(programKey string) ([]string, error)
718718
return out, rows.Err()
719719
}
720720

721+
// DeleteProgramScopeAssetsByKey clears every stored asset for a program key.
722+
func (p *PostgresDB) DeleteProgramScopeAssetsByKey(programKey string) (int64, error) {
723+
tag, err := p.pool.Exec(p.ctx, `DELETE FROM program_assets WHERE program_key = $1;`, programKey)
724+
if err != nil {
725+
return 0, fmt.Errorf("failed to delete program_assets: %v", err)
726+
}
727+
return tag.RowsAffected(), nil
728+
}
729+
721730
// RecordProgramScopeAssets diffs+stores the current asset set, returning newly-seen assets.
722731
func (p *PostgresDB) RecordProgramScopeAssets(programKey string, assets []string) ([]string, bool, error) {
723732
existing, err := p.ListProgramScopeAssets(programKey)

internal/db/sqlite.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,16 @@ func (s *SQLiteDB) ListProgramScopeAssets(programKey string) ([]string, error) {
595595
return out, rows.Err()
596596
}
597597

598+
// DeleteProgramScopeAssetsByKey clears every stored asset for a program key.
599+
func (s *SQLiteDB) DeleteProgramScopeAssetsByKey(programKey string) (int64, error) {
600+
res, err := s.db.Exec(`DELETE FROM program_assets WHERE program_key = ?;`, programKey)
601+
if err != nil {
602+
return 0, fmt.Errorf("failed to delete program_assets: %v", err)
603+
}
604+
n, _ := res.RowsAffected()
605+
return n, nil
606+
}
607+
598608
// RecordProgramScopeAssets diffs+stores the current asset set, returning newly-seen assets.
599609
func (s *SQLiteDB) RecordProgramScopeAssets(programKey string, assets []string) ([]string, bool, error) {
600610
existing, err := s.ListProgramScopeAssets(programKey)

internal/db/types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@ type DB interface {
121121
// newly-seen assets. firstRun is true when the program had no stored assets yet — in
122122
// that case the assets are baselined silently and newAssets is empty (no alert).
123123
RecordProgramScopeAssets(programKey string, assets []string) (newAssets []string, firstRun bool, err error)
124+
// DeleteProgramScopeAssetsByKey removes every asset row stored under the given key.
125+
// Used at boot to clean rows poisoned by past identifier-collision bugs (e.g. all
126+
// Intigriti programs ending up under "it:detail").
127+
DeleteProgramScopeAssetsByKey(programKey string) (int64, error)
124128

125129
// DNS Takeover Providers
126130
ListVulnerableDNSProviders() (map[string]string, error)

0 commit comments

Comments
 (0)