11package api
22
33import (
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