Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
2 changes: 1 addition & 1 deletion integration-tests/integration-test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"strings"

"github.com/logrusorgru/aurora"

"github.com/projectdiscovery/uncover/testutils"
)

Expand All @@ -33,6 +32,7 @@ var (
"odin": odinTestcases{},
"binaryedge": binaryedgeTestcases{},
"onyphe": onypheTestcases{},
"greynoise": greynoiseTestcases{},
// feature tests
"output": outputTestcases{},
}
Expand Down
28 changes: 28 additions & 0 deletions integration-tests/source-test.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,3 +264,31 @@ func (h onypheTestcases) Execute() error {
}
return expectResultsGreaterThanCount(results, 0)
}

type greynoiseTestcases struct{}

func (h greynoiseTestcases) Execute() error {
token := os.Getenv("GREYNOISE_API_KEY")
if token == "" {
return errors.New("missing greynoise api key")
}

greynoiseToken := fmt.Sprintf(`greynoise: [%s]`, token)
_ = os.WriteFile(ConfigFile, []byte(greynoiseToken), 0644)
defer os.RemoveAll(ConfigFile)

results, err := testutils.RunUncoverAndGetResults(debug, "-e", "greynoise", "-q", "tag:scanner")
if err != nil {
fmt.Fprintf(os.Stderr, "WARNING: greynoise query failed: %v\n", err)
fmt.Fprintln(os.Stderr, "INFO: This may happen if you are using a Community API key. GNQL queries require an Enterprise API key.")
return nil
}

if len(results) == 0 {
fmt.Fprintln(os.Stderr, "INFO: greynoise returned 0 results.")
fmt.Fprintln(os.Stderr, "NOTE: Community API keys cannot access GNQL queries. An Enterprise API key is required for this test to return data.")
return nil
}

return nil
}
14 changes: 10 additions & 4 deletions runner/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ type Options struct {
BinaryEdge goflags.StringSlice
Onyphe goflags.StringSlice
Driftnet goflags.StringSlice
GreyNoise goflags.StringSlice
DisableUpdateCheck bool
}

Expand All @@ -73,7 +74,7 @@ func ParseOptions() *Options {

flagSet.CreateGroup("input", "Input",
flagSet.StringSliceVarP(&options.Query, "query", "q", nil, "search query, supports: stdin,file,config input (example: -q 'example query', -q 'query.txt')", goflags.FileStringSliceOptions),
flagSet.StringSliceVarP(&options.Engine, "engine", "e", nil, "search engine to query (shodan,shodan-idb,fofa,censys,quake,hunter,zoomeye,netlas,publicwww,criminalip,hunterhow,google,odin,binaryedge,onyphe,driftnet) (default shodan)", goflags.FileNormalizedStringSliceOptions),
flagSet.StringSliceVarP(&options.Engine, "engine", "e", nil, "search engine to query (shodan,shodan-idb,fofa,censys,quake,hunter,zoomeye,netlas,publicwww,criminalip,hunterhow,google,odin,binaryedge,onyphe,driftnet,greynoise) (default shodan)", goflags.FileNormalizedStringSliceOptions),
flagSet.StringSliceVarP(&options.AwesomeSearchQueries, "awesome-search-queries", "asq", nil, "use awesome search queries to discover exposed assets on the internet (example: -asq 'jira')", goflags.FileStringSliceOptions),
)

Expand All @@ -94,6 +95,7 @@ func ParseOptions() *Options {
flagSet.StringSliceVarP(&options.BinaryEdge, "binaryedge", "be", nil, "search query for binaryedge (example: -binaryedge 'query.txt')", goflags.FileStringSliceOptions),
flagSet.StringSliceVarP(&options.Onyphe, "onyphe", "on", nil, "search query for onyphe (example: -onyphe 'query.txt')", goflags.FileStringSliceOptions),
flagSet.StringSliceVarP(&options.Driftnet, "driftnet", "df", nil, "search query for driftnet (example: -driftnet 'query.txt')", goflags.FileStringSliceOptions),
flagSet.StringSliceVarP(&options.GreyNoise, "greynoise", "gn", nil, "search query for greynoise (example: -greynoise 'query.txt')", goflags.FileStringSliceOptions),
)

flagSet.CreateGroup("config", "Config",
Expand Down Expand Up @@ -169,7 +171,8 @@ func ParseOptions() *Options {
len(options.Odin),
len(options.BinaryEdge),
len(options.Onyphe),
len(options.Driftnet)) {
len(options.Driftnet),
len(options.GreyNoise)) {
options.Engine = append(options.Engine, "shodan")
}

Expand Down Expand Up @@ -241,7 +244,8 @@ func (options *Options) validateOptions() error {
len(options.Odin),
len(options.BinaryEdge),
len(options.Onyphe),
len(options.Driftnet)) {
len(options.Driftnet),
len(options.GreyNoise)) {
return errors.New("no query provided")
}

Expand All @@ -268,7 +272,8 @@ func (options *Options) validateOptions() error {
len(options.Odin),
len(options.BinaryEdge),
len(options.Onyphe),
len(options.Driftnet)) {
len(options.Driftnet),
len(options.GreyNoise)) {
return errors.New("no engine specified")
}

Expand Down Expand Up @@ -312,6 +317,7 @@ func appendAllQueries(options *Options) {
appendQuery(options, "binaryedge", options.BinaryEdge...)
appendQuery(options, "onyphe", options.Onyphe...)
appendQuery(options, "driftnet", options.Driftnet...)
appendQuery(options, "greynoise", options.GreyNoise...)
}

func (options *Options) useAwesomeSearchQueries(awesomeSearchQueries []string) error {
Expand Down
Empty file.
188 changes: 188 additions & 0 deletions sources/agent/greynoise/greynoise.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package greynoise

import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"

"github.com/projectdiscovery/uncover/sources"
)

const (
URL = "https://api.greynoise.io/v3/gnql"
)

type Agent struct{}

func (agent *Agent) Name() string { return "greynoise" }

func (agent *Agent) Query(session *sources.Session, query *sources.Query) (chan sources.Result, error) {
if session.Keys.GreyNoiseKey == "" {
return nil, errors.New("empty GreyNoise API key")
}

results := make(chan sources.Result)

go func() {
defer close(results)

scrollToken := ""
total := 0
done := false

pageSize := 1000
if query.Limit > 0 && query.Limit < pageSize {
pageSize = query.Limit
}

for !done {
req := &Request{
Query: query.Query,
Size: pageSize,
Scroll: scrollToken,
Quick: false,
ExcludeRaw: true,
}

apiResponse, err := agent.query(session, req)
if err != nil {
results <- sources.Result{Source: agent.Name(), Error: err}
return
}
if apiResponse == nil || len(apiResponse.Data) == 0 {
return
}

for _, item := range apiResponse.Data {
host := firstNonEmpty(
item.InternetScannerIntelligence.Metadata.Domain,
item.InternetScannerIntelligence.Metadata.RDNS,
)

r := sources.Result{
Source: agent.Name(),
IP: item.IP,
Host: host,
}
if raw, err := json.Marshal(item); err == nil {
r.Raw = raw
}
results <- r

total++
if query.Limit > 0 && total >= query.Limit {
done = true
break
}
}

done = done || apiResponse.RequestMetadata.Complete
scrollToken = apiResponse.RequestMetadata.Scroll
if strings.TrimSpace(scrollToken) == "" {
done = true
}

if query.Limit > 0 && !done {
remain := query.Limit - total
if remain < pageSize {
pageSize = remain
}
}
}
}()

return results, nil
}

func (agent *Agent) query(session *sources.Session, request *Request) (*Response, error) {
params := url.Values{}
params.Set("query", request.Query)

if request.Size > 0 {
if request.Size > 10000 {
request.Size = 10000
}
params.Set("size", strconv.Itoa(request.Size))
}
if request.Scroll != "" {
params.Set("scroll", request.Scroll)
}
if request.Quick {
params.Set("quick", "true")
}
if request.ExcludeRaw {
params.Set("exclude_raw", "true")
}

fullURL := URL
if enc := params.Encode(); enc != "" {
fullURL = fullURL + "?" + enc
}

req, err := sources.NewHTTPRequest(http.MethodGet, fullURL, nil)
if err != nil {
return nil, err
}

req.Header.Set("Accept", "application/json")
req.Header.Set("key", session.Keys.GreyNoiseKey)

resp, err := session.Do(req, agent.Name())
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode < 200 || resp.StatusCode > 299 {
b, _ := io.ReadAll(resp.Body)
msg := strings.TrimSpace(string(b))

if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusNotFound {
return nil, fmt.Errorf(
"GreyNoise GNQL request failed: status=%d. Your API key may not include GNQL access (Enterprise key required). body=%s",
resp.StatusCode, msg,
)
}

return nil, fmt.Errorf("greynoise GNQL request failed: status=%d body=%s", resp.StatusCode, msg)
}

var apiResponse Response
if err := json.NewDecoder(resp.Body).Decode(&apiResponse); err != nil {
fmt.Fprintf(os.Stderr, "DEBUG: GreyNoise decode error status=%d: %v\n", resp.StatusCode, err)
return nil, err
}

fmt.Fprintf(os.Stderr,
"DEBUG: GNQL count=%d complete=%v scroll=%s data=%d msg=%s\n",
apiResponse.RequestMetadata.Count,
apiResponse.RequestMetadata.Complete,
short(apiResponse.RequestMetadata.Scroll, 12),
len(apiResponse.Data),
short(apiResponse.RequestMetadata.Message, 120),
)

return &apiResponse, nil
}

func firstNonEmpty(vs ...string) string {
for _, v := range vs {
if strings.TrimSpace(v) != "" {
return v
}
}
return ""
}

func short(s string, max int) string {
if len(s) <= max {
return s
}
return s[:max] + "…"
}
9 changes: 9 additions & 0 deletions sources/agent/greynoise/request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package greynoise

type Request struct {
Query string // GNQL query string (required)
Size int // Number of results per page (1-10000, defaults to 10000)
Scroll string // Scroll token for pagination
Quick bool // Quick=true returns only IP and classification/trust level
ExcludeRaw bool // Optional: request without heavy raw_data
}
Loading