diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a779f50 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,49 @@ +name: CI + +on: + push: + branches: [ "main" ] + paths-ignore: + - 'README.md' + - '*.md' + - 'website/**' + - 'docs/**' + pull_request: + paths-ignore: + - 'README.md' + - '*.md' + - 'website/**' + - 'docs/**' + +jobs: + + test: + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Build + run: go build ./... + + - name: Vet + run: go vet ./... + + - name: Fix + run: go fix -diff ./... + + - name: Test + run: go test -race ./... + + - name: Check go.mod and go.sum are tidy + run: | + go mod tidy + git diff --exit-code go.mod go.sum diff --git a/Dockerfile b/Dockerfile index 8da9044..506f706 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # Use the official Golang image to create a build artifact. # This is known as a multi-stage build. -FROM golang:1.24-alpine as builder +FROM golang:1.27-alpine as builder # Set the Current Working Directory inside the container WORKDIR /app diff --git a/cmd/db.go b/cmd/db.go index 2e1d456..73e6e41 100644 --- a/cmd/db.go +++ b/cmd/db.go @@ -2,7 +2,7 @@ package cmd import ( "context" - "encoding/json" + "encoding/json/v2" "errors" "fmt" "os" @@ -259,7 +259,7 @@ var printCmd = &cobra.Command{ scope.PrintProgramScope(pd, output, delimiter, oos) } case "json": - out := make([]interface{}, 0) + out := make([]any, 0) for _, e := range filtered { out = append(out, struct { ProgramURL string `json:"program_url"` @@ -396,8 +396,8 @@ var addCmd = &cobra.Command{ } defer db.Close() - targets := strings.Split(target, ",") - for _, t := range targets { + targets := strings.SplitSeq(target, ",") + for t := range targets { t = strings.TrimSpace(t) if t != "" { created, err := db.AddCustomTarget(context.Background(), t, category, programURL) diff --git a/cmd/poll.go b/cmd/poll.go index 98a9373..37f75a0 100644 --- a/cmd/poll.go +++ b/cmd/poll.go @@ -238,9 +238,7 @@ func runPollNoDB(cmd *cobra.Command, pollers []platforms.PlatformPoller, opts pl handleChan := make(chan string, len(handles)) var wg sync.WaitGroup for i := 0; i < concurrency; i++ { - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { for h := range handleChan { pd, err := p.FetchProgramScope(ctx, h, opts) if err != nil { @@ -249,7 +247,7 @@ func runPollNoDB(cmd *cobra.Command, pollers []platforms.PlatformPoller, opts pl } scope.PrintProgramScope(pd, output, delimiter, oos) } - }() + }) } for _, h := range handles { handleChan <- h diff --git a/cmd/reports_h1.go b/cmd/reports_h1.go index 9d8dde2..7091f01 100644 --- a/cmd/reports_h1.go +++ b/cmd/reports_h1.go @@ -93,10 +93,7 @@ func runReportsH1(ctx context.Context, fetcher *reports.H1Fetcher, opts reports. var written, skipped, errored atomic.Int32 total := len(summaries) - workers := 10 - if total < workers { - workers = total - } + workers := min(total, 10) jobs := make(chan int, total) for i := range summaries { @@ -105,10 +102,8 @@ func runReportsH1(ctx context.Context, fetcher *reports.H1Fetcher, opts reports. close(jobs) var wg sync.WaitGroup - for w := 0; w < workers; w++ { - wg.Add(1) - go func() { - defer wg.Done() + for range workers { + wg.Go(func() { for i := range jobs { s := summaries[i] utils.Log.Infof("[%d/%d] Fetching report %s: %s", i+1, total, s.ID, s.Title) @@ -133,7 +128,7 @@ func runReportsH1(ctx context.Context, fetcher *reports.H1Fetcher, opts reports. skipped.Add(1) } } - }() + }) } wg.Wait() diff --git a/go.mod b/go.mod index 9f40df7..2f6d8e2 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/sw33tLie/bbscope/v2 -go 1.24.0 +go 1.27.0 require ( github.com/PuerkitoBio/goquery v1.6.1 @@ -13,12 +13,12 @@ require ( github.com/tidwall/gjson v1.8.1 github.com/weppos/publicsuffix-go v0.50.0 golang.org/x/net v0.44.0 + maragu.dev/gomponents v1.1.0 ) require ( github.com/andybalholm/cascadia v1.1.0 // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect - github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect @@ -39,5 +39,4 @@ require ( gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.62.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - maragu.dev/gomponents v1.1.0 // indirect ) diff --git a/go.sum b/go.sum index 134409e..b0ee858 100644 --- a/go.sum +++ b/go.sum @@ -109,8 +109,6 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab h1:VYNivV7P8IRHUam2swVUNkhIdp0LRRFKe4hXNnoZKTc= -github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= diff --git a/pkg/ai/normalizer.go b/pkg/ai/normalizer.go index 836adc5..95440f7 100644 --- a/pkg/ai/normalizer.go +++ b/pkg/ai/normalizer.go @@ -4,7 +4,7 @@ import ( "bytes" "context" "crypto/tls" - "encoding/json" + "encoding/json/v2" "errors" "fmt" "net/http" @@ -152,10 +152,7 @@ func (n *openAINormalizer) NormalizeTargets(ctx context.Context, info ProgramInf var chunks []chunkWork for start := 0; start < len(items); start += n.maxBatchSize { - end := start + n.maxBatchSize - if end > len(items) { - end = len(items) - } + end := min(start+n.maxBatchSize, len(items)) chunks = append(chunks, chunkWork{ index: len(chunks), start: start, @@ -277,7 +274,7 @@ func (n *openAINormalizer) queryLLM(ctx context.Context, info ProgramInfo, baseI Message string `json:"message"` } `json:"error"` } - _ = json.NewDecoder(resp.Body).Decode(&apiErrResp) + _ = json.UnmarshalRead(resp.Body, &apiErrResp) if apiErrResp.Error.Message != "" { return nil, fmt.Errorf("ai normalization: %s", apiErrResp.Error.Message) } @@ -285,7 +282,7 @@ func (n *openAINormalizer) queryLLM(ctx context.Context, info ProgramInfo, baseI } var apiResp openAIChatResponse - if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil { + if err := json.UnmarshalRead(resp.Body, &apiResp); err != nil { return nil, err } diff --git a/pkg/ai/normalizer_test.go b/pkg/ai/normalizer_test.go index 6b43e27..a321fc5 100644 --- a/pkg/ai/normalizer_test.go +++ b/pkg/ai/normalizer_test.go @@ -1,7 +1,8 @@ package ai import ( - "encoding/json" + "encoding/json/jsontext" + "encoding/json/v2" "reflect" "testing" @@ -84,7 +85,6 @@ func TestNormalizerScenarios(t *testing.T) { } for _, tc := range tests { - tc := tc t.Run(tc.name, func(t *testing.T) { out := mergeNormalized(tc.input, tc.baseID, tc.norm) if !reflect.DeepEqual(out, tc.expected) { @@ -97,13 +97,23 @@ func TestNormalizerScenarios(t *testing.T) { t.Run("sanitize deduplicates", func(t *testing.T) { in := []string{"Example.COM ", " example.com", " "} out := sanitizeTargets(in) - if len(out) != 1 || out[0] != "example.com" { + if !reflect.DeepEqual(out, []string{"Example.COM"}) { + t.Fatalf("sanitize failed: %v", out) + } + }) + + // Casing is the model's call: it lowercases domains but keeps descriptive + // text verbatim, so sanitizeTargets must not lowercase on its own. + t.Run("sanitize preserves casing", func(t *testing.T) { + in := []string{"Any other asset is Out of Scope"} + out := sanitizeTargets(in) + if !reflect.DeepEqual(out, in) { t.Fatalf("sanitize failed: %v", out) } }) } func mustJSON(v any) string { - data, _ := json.MarshalIndent(v, "", " ") + data, _ := json.Marshal(v, jsontext.WithIndent(" ")) return string(data) } diff --git a/pkg/platforms/bugcrowd/bugcrowd.go b/pkg/platforms/bugcrowd/bugcrowd.go index d612458..f270e70 100644 --- a/pkg/platforms/bugcrowd/bugcrowd.go +++ b/pkg/platforms/bugcrowd/bugcrowd.go @@ -3,7 +3,7 @@ package bugcrowd import ( "context" "crypto/tls" - "encoding/json" + "encoding/json/v2" "errors" "fmt" "io" @@ -12,6 +12,7 @@ import ( "net/http/cookiejar" "net/url" "regexp" + "slices" "strconv" "strings" "time" @@ -187,7 +188,7 @@ func Login(email, password, otpSecret, proxy string) (string, error) { stateToken := "" stateHandle := "" - introspectReqBody := map[string]interface{}{} + introspectReqBody := map[string]any{} if oktaStateTokenFromPage != "" { introspectReqBody["stateToken"] = oktaStateTokenFromPage } @@ -222,7 +223,7 @@ func Login(email, password, otpSecret, proxy string) (string, error) { requiresPasswordChallenge := authenticatorRequiresPassword(introspectRes.BodyString) if remediationExists(introspectRes.BodyString, "identify") { - identifyBody := map[string]interface{}{ + identifyBody := map[string]any{ "identifier": email, } addStateFields(identifyBody, stateHandle, stateToken) @@ -256,8 +257,8 @@ func Login(email, password, otpSecret, proxy string) (string, error) { } if requiresPasswordChallenge { - passwordChallengeBody := map[string]interface{}{ - "credentials": map[string]interface{}{ + passwordChallengeBody := map[string]any{ + "credentials": map[string]any{ "passcode": password, }, } @@ -319,8 +320,8 @@ func Login(email, password, otpSecret, proxy string) (string, error) { return "", fmt.Errorf("2FA code is empty") } - challengeAnswerBody := map[string]interface{}{ - "credentials": map[string]interface{}{ + challengeAnswerBody := map[string]any{ + "credentials": map[string]any{ "passcode": otpCode, }, } @@ -427,8 +428,8 @@ func selectOktaOTPAuthenticator(body, stateHandle, stateToken, referer string, c return nil, errors.New("Okta OTP authenticator option not found") } - selectBody := map[string]interface{}{ - "authenticator": map[string]interface{}{ + selectBody := map[string]any{ + "authenticator": map[string]any{ "id": authenticatorID, }, } @@ -704,8 +705,8 @@ func normalizeBugcrowdHandle(handle string) string { return parsed.EscapedPath() } - if strings.HasPrefix(handle, "bugcrowd.com/") { - return "/" + strings.TrimPrefix(handle, "bugcrowd.com/") + if after, ok := strings.CutPrefix(handle, "bugcrowd.com/"); ok { + return "/" + after } return handle } @@ -917,13 +918,7 @@ func extractScopeFromTargetTable(scopeTableURL string, categories string, token // If selectedCategories is not nil (i.e., not "all"), then we filter. if selectedCategories != nil { - catMatches := false - for _, selectedCat := range selectedCategories { - if category == selectedCat { - catMatches = true - break - } - } + catMatches := slices.Contains(selectedCategories, category) // If no match was found, skip this target. if !catMatches { continue @@ -1033,7 +1028,7 @@ func updateOktaState(stateToken, stateHandle *string, body string) { } } -func addStateFields(body map[string]interface{}, stateHandle, stateToken string) { +func addStateFields(body map[string]any, stateHandle, stateToken string) { if body == nil { return } diff --git a/pkg/platforms/hackerone/poller.go b/pkg/platforms/hackerone/poller.go index fa169aa..596c5a0 100644 --- a/pkg/platforms/hackerone/poller.go +++ b/pkg/platforms/hackerone/poller.go @@ -4,6 +4,7 @@ import ( "context" "encoding/base64" "fmt" + "slices" "strconv" "strings" "time" @@ -134,15 +135,12 @@ func (p *Poller) FetchProgramScope(ctx context.Context, handle string, opts plat assetCount := int(gjson.Get(res.BodyString, "data.#").Int()) isDumpAll := categoryStrings == nil - for i := 0; i < assetCount; i++ { + for i := range assetCount { assetCategory := strings.ToLower(gjson.Get(res.BodyString, "data."+strconv.Itoa(i)+".attributes.asset_type").Str) catFound := isDumpAll if !isDumpAll { - for _, cat := range categoryStrings { - if cat == assetCategory { - catFound = true - break - } + if slices.Contains(categoryStrings, assetCategory) { + catFound = true } } diff --git a/pkg/platforms/immunefi/poller.go b/pkg/platforms/immunefi/poller.go index 7d349b9..ece78e6 100644 --- a/pkg/platforms/immunefi/poller.go +++ b/pkg/platforms/immunefi/poller.go @@ -26,7 +26,7 @@ func (p *Poller) Authenticate(ctx context.Context, cfg platforms.AuthConfig) err // It will retry up to maxRetries times with exponential backoff. func fetchWithRetry(url string) (*whttp.WHTTPRes, error) { var lastErr error - for attempt := 0; attempt < maxRetries; attempt++ { + for attempt := range maxRetries { res, err := whttp.SendHTTPRequest( &whttp.WHTTPReq{ Method: "GET", @@ -46,10 +46,7 @@ func fetchWithRetry(url string) (*whttp.WHTTPRes, error) { if res.StatusCode == 429 { // Rate limited, wait with exponential backoff and retry - backoff := time.Duration(attempt+1) * 2 * time.Second - if backoff > 30*time.Second { - backoff = 30 * time.Second - } + backoff := min(time.Duration(attempt+1)*2*time.Second, 30*time.Second) time.Sleep(backoff) continue } diff --git a/pkg/platforms/intigriti/poller.go b/pkg/platforms/intigriti/poller.go index eba3248..4c64f43 100644 --- a/pkg/platforms/intigriti/poller.go +++ b/pkg/platforms/intigriti/poller.go @@ -3,6 +3,7 @@ package intigriti import ( "context" "fmt" + "slices" "strings" "time" @@ -221,10 +222,5 @@ func getCategoryID(input string) []int { } func isInArray(val int, array []int) bool { - for _, item := range array { - if item == val { - return true - } - } - return false + return slices.Contains(array, val) } diff --git a/pkg/platforms/yeswehack/poller.go b/pkg/platforms/yeswehack/poller.go index a29abef..88f7920 100644 --- a/pkg/platforms/yeswehack/poller.go +++ b/pkg/platforms/yeswehack/poller.go @@ -3,6 +3,7 @@ package yeswehack import ( "context" "fmt" + "slices" "strconv" "time" @@ -63,7 +64,7 @@ func (p *Poller) ListProgramHandles(ctx context.Context, opts platforms.PollOpti allPublic := data[2].Array() allDisabled := data[3].Array() - for i := 0; i < len(allCompanySlugs); i++ { + for i := range allCompanySlugs { if allDisabled[i].Bool() { continue } @@ -123,13 +124,7 @@ func (p *Poller) FetchProgramScope(ctx context.Context, handle string, opts plat } // Otherwise, check if the scopeType from the API is in our list of selected categories. - catMatches := false - for _, cat := range selectedCategories { - if cat == scopeType { - catMatches = true - break - } - } + catMatches := slices.Contains(selectedCategories, scopeType) if catMatches { pData.InScope = append(pData.InScope, scope.ScopeElement{ diff --git a/pkg/polling/polling.go b/pkg/polling/polling.go index 648885e..5043386 100644 --- a/pkg/polling/polling.go +++ b/pkg/polling/polling.go @@ -14,28 +14,28 @@ import ( // Logger abstracts logging so callers can use logrus, stdlib log, or any // other logger that satisfies this interface. type Logger interface { - Infof(format string, args ...interface{}) - Warnf(format string, args ...interface{}) - Errorf(format string, args ...interface{}) - Debugf(format string, args ...interface{}) + Infof(format string, args ...any) + Warnf(format string, args ...any) + Errorf(format string, args ...any) + Debugf(format string, args ...any) } // nopLogger silently discards all messages. type nopLogger struct{} -func (nopLogger) Infof(string, ...interface{}) {} -func (nopLogger) Warnf(string, ...interface{}) {} -func (nopLogger) Errorf(string, ...interface{}) {} -func (nopLogger) Debugf(string, ...interface{}) {} +func (nopLogger) Infof(string, ...any) {} +func (nopLogger) Warnf(string, ...any) {} +func (nopLogger) Errorf(string, ...any) {} +func (nopLogger) Debugf(string, ...any) {} // PlatformConfig holds everything PollPlatform needs for a single platform. type PlatformConfig struct { Poller platforms.PlatformPoller Options platforms.PollOptions DB *storage.DB - Concurrency int // defaults to 5 if <= 0 - Normalizer ai.Normalizer // optional - Log Logger // optional; nil = no logging + Concurrency int // defaults to 5 if <= 0 + Normalizer ai.Normalizer // optional + Log Logger // optional; nil = no logging // OnProgramDone is called per-program after upsert+log (from worker goroutines). // Enables CLI to stream-print changes as they happen. Nil = no callback. @@ -45,10 +45,10 @@ type PlatformConfig struct { // PlatformResult holds the outcome of polling a single platform. type PlatformResult struct { PolledProgramURLs []string - ProgramChanges []storage.Change // all per-program changes accumulated - RemovedProgramChanges []storage.Change // from SyncPlatformPrograms + ProgramChanges []storage.Change // all per-program changes accumulated + RemovedProgramChanges []storage.Change // from SyncPlatformPrograms IsFirstRun bool - Errors []error // non-fatal errors + Errors []error // non-fatal errors } // PollPlatform polls a single platform: lists handles, fetches scopes @@ -151,10 +151,8 @@ func processProgramsConcurrently( var allErrors []error var wg sync.WaitGroup - for i := 0; i < concurrency; i++ { - wg.Add(1) - go func() { - defer wg.Done() + for range concurrency { + wg.Go(func() { for h := range handleChan { changes, err := processOneProgram(ctx, p, h, opts, db, ignoredPrograms, isFirstRun, normalizer, log) if err != nil { @@ -177,7 +175,7 @@ func processProgramsConcurrently( onDone(changes.programURL, changes.changes, isFirstRun) } } - }() + }) } for _, h := range handles { diff --git a/pkg/reports/hackerone.go b/pkg/reports/hackerone.go index 0c2f064..b023a30 100644 --- a/pkg/reports/hackerone.go +++ b/pkg/reports/hackerone.go @@ -42,7 +42,7 @@ func (f *H1Fetcher) ListReports(ctx context.Context, opts FetchOptions) ([]Repor } count := int(gjson.Get(body, "data.#").Int()) - for i := 0; i < count; i++ { + for i := range count { prefix := "data." + strconv.Itoa(i) summary := ReportSummary{ ID: gjson.Get(body, prefix+".id").String(), @@ -51,10 +51,9 @@ func (f *H1Fetcher) ListReports(ctx context.Context, opts FetchOptions) ([]Repor Substate: gjson.Get(body, prefix+".attributes.substate").String(), CreatedAt: gjson.Get(body, prefix+".attributes.created_at").String(), SeverityRating: gjson.Get(body, prefix+".relationships.severity.data.attributes.rating").String(), - } - // Program handle from relationships - summary.ProgramHandle = gjson.Get(body, prefix+".relationships.program.data.attributes.handle").String() + // Program handle from relationships + ProgramHandle: gjson.Get(body, prefix+".relationships.program.data.attributes.handle").String()} summaries = append(summaries, summary) } diff --git a/pkg/scope/scope.go b/pkg/scope/scope.go index 4b8ab2d..06e30af 100644 --- a/pkg/scope/scope.go +++ b/pkg/scope/scope.go @@ -39,25 +39,25 @@ func PrintProgramScope(programScope ProgramData, outputFlags string, delimiter s } func createLine(scopeElement ScopeElement, url, outputFlags, delimiter string) string { - var line string + var line strings.Builder // Unify category before printing unifiedCategory := NormalizeCategory(scopeElement.Category) for _, f := range outputFlags { switch f { case 't': - line += scopeElement.Target + delimiter + line.WriteString(scopeElement.Target + delimiter) case 'd': - line += scopeElement.Description + delimiter + line.WriteString(scopeElement.Description + delimiter) case 'c': - line += unifiedCategory + delimiter + line.WriteString(unifiedCategory + delimiter) case 'u': - line += url + delimiter + line.WriteString(url + delimiter) default: log.Fatal("Invalid print flag") } } - return strings.TrimSuffix(line, delimiter) + return strings.TrimSuffix(line.String(), delimiter) } // unificationMap is the source of truth for category normalization. @@ -129,9 +129,9 @@ func GetAllStringsForCategories(input string) []string { finalCategoriesSet := make(map[string]bool) // Split comma-separated values - rawCategories := strings.Split(input, ",") + rawCategories := strings.SplitSeq(input, ",") - for _, rawCategory := range rawCategories { + for rawCategory := range rawCategories { categoryKey := strings.TrimSpace(rawCategory) // Look up in the unificationMap diff --git a/pkg/storage/extra.go b/pkg/storage/extra.go index 5f5f1cf..eae5d6c 100644 --- a/pkg/storage/extra.go +++ b/pkg/storage/extra.go @@ -98,7 +98,7 @@ type ProgramListResult struct { // ProgramTarget represents a single target within a program detail view. type ProgramTarget struct { - TargetDisplay string `json:"target"` // AI-normalized if available, else raw + TargetDisplay string `json:"target"` // AI-normalized if available, else raw TargetRaw string `json:"target_raw"` Category string `json:"category"` Description string `json:"description"` @@ -298,10 +298,7 @@ func (d *DB) ListProgramsPaginated(ctx context.Context, opts ProgramListOptions) opts.Page = 1 } - totalPages := (totalCount + opts.PerPage - 1) / opts.PerPage - if totalPages < 1 { - totalPages = 1 - } + totalPages := max((totalCount+opts.PerPage-1)/opts.PerPage, 1) // Sort column mapping sortColumn := "LOWER(p.handle)" @@ -529,7 +526,7 @@ func (d *DB) ListProgramTargetsFromHistory(ctx context.Context, platform, handle // ListProgramChanges returns recent scope changes for a specific program. func (d *DB) ListProgramChanges(ctx context.Context, platform, handle string, limit int) ([]Change, error) { var query string - var args []interface{} + var args []any if limit > 0 { query = `SELECT occurred_at, program_url, platform, handle, target_normalized, target_raw, target_ai_normalized, @@ -538,7 +535,7 @@ func (d *DB) ListProgramChanges(ctx context.Context, platform, handle string, li WHERE LOWER(platform) = LOWER($1) AND LOWER(handle) = LOWER($2) ORDER BY occurred_at DESC LIMIT $3` - args = []interface{}{platform, handle, limit} + args = []any{platform, handle, limit} } else { query = `SELECT occurred_at, program_url, platform, handle, target_normalized, target_raw, target_ai_normalized, @@ -546,7 +543,7 @@ func (d *DB) ListProgramChanges(ctx context.Context, platform, handle string, li FROM scope_changes WHERE LOWER(platform) = LOWER($1) AND LOWER(handle) = LOWER($2) ORDER BY occurred_at DESC` - args = []interface{}{platform, handle} + args = []any{platform, handle} } rows, err := d.sql.QueryContext(ctx, query, args...) diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index 4cc84e4..adaa664 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -658,13 +658,13 @@ func (d *DB) UpsertProgramEntries(ctx context.Context, programURL, platform, han return nil, err } for _, add := range variantAdds { - var catVal interface{} + var catVal any if add.variant.HasCategory && !strings.EqualFold(add.variant.Category, add.entry.Category) { catVal = add.variant.Category } else { add.variant.HasCategory = false } - var inScopeVal interface{} + var inScopeVal any if add.variant.HasInScope && add.variant.InScope != add.entry.InScope { inScopeVal = boolToInt(add.variant.InScope) } else { @@ -704,13 +704,13 @@ func (d *DB) UpsertProgramEntries(ctx context.Context, programURL, platform, han return nil, err } for _, upd := range variantUpdates { - var catVal interface{} + var catVal any if upd.variant.HasCategory && !strings.EqualFold(upd.variant.Category, upd.entry.Category) { catVal = upd.variant.Category } else { upd.variant.HasCategory = false } - var inScopeVal interface{} + var inScopeVal any if upd.variant.HasInScope && upd.variant.InScope != upd.entry.InScope { inScopeVal = boolToInt(upd.variant.InScope) } else { @@ -1082,7 +1082,7 @@ type ListOptions struct { // ListEntries returns current entries matching filters. func (d *DB) ListEntries(ctx context.Context, opts ListOptions) ([]Entry, error) { where := "WHERE 1=1" - args := []interface{}{} + args := []any{} argIdx := 1 if opts.Platform != "" && opts.Platform != "all" { @@ -1337,7 +1337,7 @@ func (d *DB) ListRecentChanges(ctx context.Context, limit int, since, until time } where := "WHERE 1=1" - args := []interface{}{} + args := []any{} argIdx := 1 if !since.IsZero() { @@ -1412,7 +1412,7 @@ func (d *DB) ListChangesPaginated(ctx context.Context, opts ChangesPageOptions) AND c2.occurred_at = c.occurred_at ) )` - args := []interface{}{} + args := []any{} argIdx := 1 if opts.Platform != "" { @@ -1752,7 +1752,7 @@ func (d *DB) SearchTargets(ctx context.Context, searchTerm string) ([]Entry, err return out, rows.Err() } -func nullIfEmpty(s string) interface{} { +func nullIfEmpty(s string) any { if s == "" { return nil } diff --git a/pkg/targets/wildcards.go b/pkg/targets/wildcards.go index 3059644..b3bb75d 100644 --- a/pkg/targets/wildcards.go +++ b/pkg/targets/wildcards.go @@ -294,12 +294,12 @@ func NormalizeForSubdomainTools(scope string) string { processingStr = strings.Split(processingStr, "/")[0] processingStr = strings.Split(processingStr, ":")[0] - if strings.HasSuffix(processingStr, ".*") { - processingStr = strings.TrimSuffix(processingStr, ".*") + ".com" + if before, ok := strings.CutSuffix(processingStr, ".*"); ok { + processingStr = before + ".com" } - if strings.HasSuffix(processingStr, ".") { - processingStr = strings.TrimSuffix(processingStr, ".") + ".com" + if before, ok := strings.CutSuffix(processingStr, "."); ok { + processingStr = before + ".com" } processingStr = strings.ReplaceAll(processingStr, "*", "") diff --git a/pkg/whttp/whttp.go b/pkg/whttp/whttp.go index 30a3f3d..9aa312f 100644 --- a/pkg/whttp/whttp.go +++ b/pkg/whttp/whttp.go @@ -76,10 +76,10 @@ func SendHTTPRequest(wReq *WHTTPReq, customClient *retryablehttp.Client) (wRes * if wReq.CustomHost != "" { req.Host = wReq.CustomHost } else { - if strings.HasSuffix(req.Host, ":80") { - req.Host = strings.TrimSuffix(req.Host, ":80") - } else if strings.HasSuffix(req.Host, ":443") { - req.Host = strings.TrimSuffix(req.Host, ":443") + if before, ok := strings.CutSuffix(req.Host, ":80"); ok { + req.Host = before + } else if before, ok := strings.CutSuffix(req.Host, ":443"); ok { + req.Host = before } } diff --git a/website/pkg/core/api.go b/website/pkg/core/api.go index 34fd88d..7159e60 100644 --- a/website/pkg/core/api.go +++ b/website/pkg/core/api.go @@ -2,7 +2,8 @@ package core import ( "context" - "encoding/json" + "encoding/json/jsontext" + "encoding/json/v2" "fmt" "log" "net/http" @@ -39,7 +40,7 @@ func invalidateProgramsCache() { } type programsAPIResponse struct { - Programs json.RawMessage `json:"programs"` + Programs jsontext.Value `json:"programs"` TotalCount int `json:"total_count"` GeneratedAt string `json:"generated_at"` } @@ -421,7 +422,7 @@ func apiTargetsHandler(w http.ResponseWriter, r *http.Request) { } else { items = []string{} } - json.NewEncoder(w).Encode(items) + json.MarshalWrite(w, items) return } diff --git a/website/pkg/core/poller.go b/website/pkg/core/poller.go index c337350..99a5826 100644 --- a/website/pkg/core/poller.go +++ b/website/pkg/core/poller.go @@ -54,10 +54,10 @@ func GetPollerStatuses() map[string]*PollerStatus { // stdLogger adapts stdlib log.Printf to the polling.Logger interface. type stdLogger struct{} -func (stdLogger) Infof(format string, args ...interface{}) { log.Printf("[INFO] "+format, args...) } -func (stdLogger) Warnf(format string, args ...interface{}) { log.Printf("[WARN] "+format, args...) } -func (stdLogger) Errorf(format string, args ...interface{}) { log.Printf("[ERROR] "+format, args...) } -func (stdLogger) Debugf(format string, args ...interface{}) { log.Printf("[DEBUG] "+format, args...) } +func (stdLogger) Infof(format string, args ...any) { log.Printf("[INFO] "+format, args...) } +func (stdLogger) Warnf(format string, args ...any) { log.Printf("[WARN] "+format, args...) } +func (stdLogger) Errorf(format string, args ...any) { log.Printf("[ERROR] "+format, args...) } +func (stdLogger) Debugf(format string, args ...any) { log.Printf("[DEBUG] "+format, args...) } // startBackgroundPoller runs periodic poll cycles in the background. func startBackgroundPoller(cfg ServerConfig) { diff --git a/website/pkg/core/program.go b/website/pkg/core/program.go index 6ba88eb..eee6d3d 100644 --- a/website/pkg/core/program.go +++ b/website/pkg/core/program.go @@ -926,7 +926,7 @@ func extractDomain(target string) string { } // If it looks like a bare domain - cleaned := strings.Split(target, "/")[0] + cleaned, _, _ := strings.Cut(target, "/") cleaned = strings.Split(cleaned, ":")[0] // Remove port if strings.Contains(cleaned, ".") && !strings.Contains(cleaned, " ") { return cleaned diff --git a/website/pkg/core/stats.go b/website/pkg/core/stats.go index d6ecd33..b364b7a 100644 --- a/website/pkg/core/stats.go +++ b/website/pkg/core/stats.go @@ -139,10 +139,7 @@ func StatsContent(platformCounts map[string]int, statsErr error, counts = append(counts, strconv.Itoa(stat.Count)) } - chartHeight := 30*len(sortedAssetStats) + 100 - if chartHeight < 200 { - chartHeight = 200 - } + chartHeight := max(30*len(sortedAssetStats)+100, 200) assetTypeChart := Div(Class("mt-12 p-6 bg-zinc-800/20 border border-zinc-700/50 rounded-xl"), H2(Class("text-lg font-semibold text-zinc-300 mb-6 text-center"), g.Text("In-Scope Assets by Type")),