Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -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
Expand Down
8 changes: 4 additions & 4 deletions cmd/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package cmd

import (
"context"
"encoding/json"
"encoding/json/v2"
"errors"
"fmt"
"os"
Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 2 additions & 4 deletions cmd/poll.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down
13 changes: 4 additions & 9 deletions cmd/reports_h1.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand All @@ -133,7 +128,7 @@ func runReportsH1(ctx context.Context, fetcher *reports.H1Fetcher, opts reports.
skipped.Add(1)
}
}
}()
})
}
wg.Wait()

Expand Down
5 changes: 2 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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
)
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
11 changes: 4 additions & 7 deletions pkg/ai/normalizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"encoding/json/v2"
"errors"
"fmt"
"net/http"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -277,15 +274,15 @@ 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)
}
return nil, fmt.Errorf("ai normalization failed with HTTP %d", resp.StatusCode)
}

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
}

Expand Down
18 changes: 14 additions & 4 deletions pkg/ai/normalizer_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package ai

import (
"encoding/json"
"encoding/json/jsontext"
"encoding/json/v2"
"reflect"
"testing"

Expand Down Expand Up @@ -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) {
Expand All @@ -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)
}
33 changes: 14 additions & 19 deletions pkg/platforms/bugcrowd/bugcrowd.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package bugcrowd
import (
"context"
"crypto/tls"
"encoding/json"
"encoding/json/v2"
"errors"
"fmt"
"io"
Expand All @@ -12,6 +12,7 @@ import (
"net/http/cookiejar"
"net/url"
"regexp"
"slices"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
},
}
Expand Down Expand Up @@ -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,
},
}
Expand Down Expand Up @@ -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,
},
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
10 changes: 4 additions & 6 deletions pkg/platforms/hackerone/poller.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/base64"
"fmt"
"slices"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -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
}
}

Expand Down
Loading
Loading