Skip to content

Commit 9a8a4ee

Browse files
h0tak88rclaude
andcommitted
feat(programs): GET /api/scope/debug — surface raw H1 response + parser output
When the watch silently doesn't alert (Dialogue added 4 librechat URLs Jun 23 but the dashboard didn't reflect it), we need to see whether: H1 is rate-limiting, the new assets are present but under unexpected field names, or our parser is dropping them. The previous answer was "guess and read code" — now you can curl: curl -s -u admin:PASS 'https://<host>/api/scope/debug?platform=h1&handle=dialogue' | jq Returns: HTTP status + rate-limit headers (Retry-After / X-Rate-Limit-Remaining / Reset), the first 10 raw scope entries from H1 (with their actual field names), how many scope_entries_returned, AND what our parser extracted (latest_target, latest_target_updated_at, all_assets_count, ok flag). At-a-glance answer to "is H1 sending it / does our parser see it". Only platform=h1 supported initially; sits behind the existing auth middleware. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent f03ef72 commit 9a8a4ee

2 files changed

Lines changed: 84 additions & 0 deletions

File tree

internal/api/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,7 @@ func SetupAPI() *gin.Engine {
583583
apiGroup.POST("/scope/program-summaries", apiProgramScopeSummaries)
584584
apiGroup.GET("/scope/watch-status", apiProgramWatchStatus)
585585
apiGroup.POST("/scope/watch-test", apiProgramWatchTest)
586+
apiGroup.GET("/scope/debug", apiProgramScopeDebug)
586587
// AI finding validation & reporting
587588
apiGroup.POST("/findings/validate", apiValidateFinding)
588589
apiGroup.POST("/findings/report", apiReportFinding)

internal/api/program_watch.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package api
22

33
import (
4+
"encoding/base64"
5+
"encoding/json"
46
"fmt"
7+
"io"
58
"net/http"
69
"os"
710
"sort"
@@ -13,6 +16,7 @@ import (
1316
"github.com/h0tak88r/AutoAR/internal/db"
1417
"github.com/h0tak88r/AutoAR/internal/logger"
1518
"github.com/h0tak88r/AutoAR/internal/utils"
19+
"github.com/tidwall/gjson"
1620
)
1721

1822
// ─── Passive program-scope watch ───────────────────────────────────────────────
@@ -319,3 +323,82 @@ func apiProgramWatchTest(c *gin.Context) {
319323
programWatchMu.Unlock()
320324
c.JSON(http.StatusOK, gin.H{"ok": true})
321325
}
326+
327+
// GET /api/scope/debug?platform=h1&handle=dialogue
328+
// Fetches a single program's scope live and returns BOTH the raw H1 response
329+
// (first 10 scope entries) AND what our parser extracts, so a "we're missing
330+
// the new assets" report can be diagnosed without guessing — you can see at a
331+
// glance whether H1 is rate-limiting, whether the assets are there but under
332+
// unexpected field names, or whether our parser is dropping them.
333+
func apiProgramScopeDebug(c *gin.Context) {
334+
platform := strings.ToLower(strings.TrimSpace(c.Query("platform")))
335+
handle := strings.TrimSpace(c.Query("handle"))
336+
if handle == "" {
337+
c.JSON(http.StatusBadRequest, gin.H{"error": "handle query param required"})
338+
return
339+
}
340+
if platform == "" {
341+
platform = "h1"
342+
}
343+
if platform != "h1" {
344+
c.JSON(http.StatusBadRequest, gin.H{"error": "only platform=h1 supported currently"})
345+
return
346+
}
347+
348+
u, t := os.Getenv("H1_USERNAME"), os.Getenv("H1_TOKEN")
349+
if u == "" || t == "" {
350+
c.JSON(http.StatusBadRequest, gin.H{"error": "H1_USERNAME / H1_TOKEN not set"})
351+
return
352+
}
353+
auth := base64.StdEncoding.EncodeToString([]byte(u + ":" + t))
354+
355+
url := fmt.Sprintf("https://api.hackerone.com/v1/hackers/programs/%s/structured_scopes?page%%5Bsize%%5D=100", handle)
356+
req, _ := http.NewRequest("GET", url, nil)
357+
req.Header.Set("Accept", "application/json")
358+
req.Header.Set("Authorization", "Basic "+auth)
359+
resp, err := (&http.Client{Timeout: 15 * time.Second}).Do(req)
360+
if err != nil {
361+
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
362+
return
363+
}
364+
body, _ := io.ReadAll(resp.Body)
365+
resp.Body.Close()
366+
367+
out := gin.H{
368+
"request_url": url,
369+
"status_code": resp.StatusCode,
370+
"headers": gin.H{
371+
"retry_after": resp.Header.Get("Retry-After"),
372+
"ratelimit_remaining": resp.Header.Get("X-Rate-Limit-Remaining"),
373+
"ratelimit_reset": resp.Header.Get("X-Rate-Limit-Reset"),
374+
},
375+
}
376+
377+
// Capture the first 10 raw scope entries so we can see what field names
378+
// (and values) H1 is actually sending for this program.
379+
var rawScopes []any
380+
data := gjson.GetBytes(body, "data").Array()
381+
out["scope_entries_returned"] = len(data)
382+
for i, s := range data {
383+
if i >= 10 {
384+
break
385+
}
386+
var entry any
387+
if err := json.Unmarshal([]byte(s.Raw), &entry); err == nil {
388+
rawScopes = append(rawScopes, entry)
389+
}
390+
}
391+
out["raw_scope_entries_sample"] = rawScopes
392+
393+
// What our parser would extract (or skip, if H1 returned non-200).
394+
parsed, ok := fetchH1ScopeSummary(handle, auth)
395+
out["parser_output"] = gin.H{
396+
"ok": ok,
397+
"scope_targets": parsed.ScopeTargets,
398+
"latest_target": parsed.LatestTarget,
399+
"latest_target_updated_at": parsed.LatestTargetUpdatedAt,
400+
"all_assets_count": len(parsed.Assets),
401+
}
402+
403+
c.JSON(http.StatusOK, out)
404+
}

0 commit comments

Comments
 (0)