Enforce Go style rules across codebase
Apply five style rules: convert iota string enums to typed string constants, replace errors.As with errors.AsType, merge three-group imports into two groups, fix multiline parameter/argument formatting, and replace fmt.Sprintf URL construction with net/url. Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
@@ -54,56 +54,70 @@ func DownloadPDFTool() agent.Tool {
|
||||
"Download a PDF document from a URL and extract its text content. Use this for DPAs, SOC 2 reports, privacy policies, and other documents hosted as PDFs.",
|
||||
func(ctx context.Context, p downloadPDFParams) (agent.ToolResult, error) {
|
||||
if err := validatePublicURL(p.URL); err != nil {
|
||||
return agent.ResultJSON(downloadPDFResult{
|
||||
ErrorDetail: fmt.Sprintf("URL not allowed: %s", err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
downloadPDFResult{
|
||||
ErrorDetail: fmt.Sprintf("URL not allowed: %s", err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, p.URL, nil)
|
||||
if err != nil {
|
||||
return agent.ResultJSON(downloadPDFResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot create request: %s", err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
downloadPDFResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot create request: %s", err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return agent.ResultJSON(downloadPDFResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot download PDF: %s", err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
downloadPDFResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot download PDF: %s", err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return agent.ResultJSON(downloadPDFResult{
|
||||
ErrorDetail: fmt.Sprintf("PDF download returned status %d", resp.StatusCode),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
downloadPDFResult{
|
||||
ErrorDetail: fmt.Sprintf("PDF download returned status %d", resp.StatusCode),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
// Read PDF into memory (max 20MB).
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 20*1024*1024))
|
||||
if err != nil {
|
||||
return agent.ResultJSON(downloadPDFResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot read PDF body: %s", err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
downloadPDFResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot read PDF body: %s", err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
// Write to temp file for pdfcpu.
|
||||
tmpDir, err := os.MkdirTemp("", "pdf-extract-*")
|
||||
if err != nil {
|
||||
return agent.ResultJSON(downloadPDFResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot create temp dir: %s", err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
downloadPDFResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot create temp dir: %s", err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||
|
||||
tmpFile := filepath.Join(tmpDir, "input.pdf")
|
||||
if err := os.WriteFile(tmpFile, body, 0o600); err != nil {
|
||||
return agent.ResultJSON(downloadPDFResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot write temp file: %s", err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
downloadPDFResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot write temp file: %s", err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
// Get page count.
|
||||
@@ -111,24 +125,30 @@ func DownloadPDFTool() agent.Tool {
|
||||
|
||||
pageCount, err := api.PageCountFile(tmpFile)
|
||||
if err != nil {
|
||||
return agent.ResultJSON(downloadPDFResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot read PDF: %s", err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
downloadPDFResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot read PDF: %s", err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
// Extract content to output dir.
|
||||
outDir := filepath.Join(tmpDir, "out")
|
||||
if err := os.MkdirAll(outDir, 0o700); err != nil {
|
||||
return agent.ResultJSON(downloadPDFResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot create output dir: %s", err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
downloadPDFResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot create output dir: %s", err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
reader := bytes.NewReader(body)
|
||||
if err := api.ExtractContent(reader, outDir, "content", nil, conf); err != nil {
|
||||
return agent.ResultJSON(downloadPDFResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot extract PDF content: %s", err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
downloadPDFResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot extract PDF content: %s", err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
// Read all extracted content files.
|
||||
@@ -154,10 +174,12 @@ func DownloadPDFTool() agent.Tool {
|
||||
text = text[:maxTextLength] + "\n[... truncated]"
|
||||
}
|
||||
|
||||
return agent.ResultJSON(downloadPDFResult{
|
||||
Text: text,
|
||||
PageCount: pageCount,
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
downloadPDFResult{
|
||||
Text: text,
|
||||
PageCount: pageCount,
|
||||
},
|
||||
), nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -46,37 +47,49 @@ func FetchRobotsTxtTool() agent.Tool {
|
||||
"Fetch and parse the robots.txt file for a domain. Returns sitemap URLs and disallowed paths, which can reveal hidden pages the crawler might miss.",
|
||||
func(ctx context.Context, p robotsParams) (agent.ToolResult, error) {
|
||||
if err := validatePublicDomain(p.Domain); err != nil {
|
||||
return agent.ResultJSON(robotsResult{
|
||||
Found: false,
|
||||
ErrorDetail: fmt.Sprintf("domain not allowed: %s", err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
robotsResult{
|
||||
Found: false,
|
||||
ErrorDetail: fmt.Sprintf("domain not allowed: %s", err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
u := "https://" + p.Domain + "/robots.txt"
|
||||
u := &url.URL{
|
||||
Scheme: "https",
|
||||
Host: p.Domain,
|
||||
Path: "/robots.txt",
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return agent.ResultJSON(robotsResult{
|
||||
Found: false,
|
||||
ErrorDetail: fmt.Sprintf("cannot create request: %s", err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
robotsResult{
|
||||
Found: false,
|
||||
ErrorDetail: fmt.Sprintf("cannot create request: %s", err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return agent.ResultJSON(robotsResult{
|
||||
Found: false,
|
||||
ErrorDetail: fmt.Sprintf("cannot fetch robots.txt: %s", err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
robotsResult{
|
||||
Found: false,
|
||||
ErrorDetail: fmt.Sprintf("cannot fetch robots.txt: %s", err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return agent.ResultJSON(robotsResult{
|
||||
Found: false,
|
||||
ErrorDetail: fmt.Sprintf("robots.txt returned status %d", resp.StatusCode),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
robotsResult{
|
||||
Found: false,
|
||||
ErrorDetail: fmt.Sprintf("robots.txt returned status %d", resp.StatusCode),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
var result robotsResult
|
||||
|
||||
@@ -52,35 +52,43 @@ func FetchSitemapTool() agent.Tool {
|
||||
"Fetch and parse a sitemap XML file. Returns discovered URLs which can reveal pages not linked from the main navigation (trust centers, legal docs, status pages).",
|
||||
func(ctx context.Context, p sitemapParams) (agent.ToolResult, error) {
|
||||
if err := validatePublicURL(p.URL); err != nil {
|
||||
return agent.ResultJSON(sitemapResult{
|
||||
Found: false,
|
||||
ErrorDetail: fmt.Sprintf("URL not allowed: %s", err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
sitemapResult{
|
||||
Found: false,
|
||||
ErrorDetail: fmt.Sprintf("URL not allowed: %s", err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, p.URL, nil)
|
||||
if err != nil {
|
||||
return agent.ResultJSON(sitemapResult{
|
||||
Found: false,
|
||||
ErrorDetail: fmt.Sprintf("cannot create request: %s", err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
sitemapResult{
|
||||
Found: false,
|
||||
ErrorDetail: fmt.Sprintf("cannot create request: %s", err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return agent.ResultJSON(sitemapResult{
|
||||
Found: false,
|
||||
ErrorDetail: fmt.Sprintf("cannot fetch sitemap: %s", err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
sitemapResult{
|
||||
Found: false,
|
||||
ErrorDetail: fmt.Sprintf("cannot fetch sitemap: %s", err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return agent.ResultJSON(sitemapResult{
|
||||
Found: false,
|
||||
ErrorDetail: fmt.Sprintf("sitemap returned status %d", resp.StatusCode),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
sitemapResult{
|
||||
Found: false,
|
||||
ErrorDetail: fmt.Sprintf("sitemap returned status %d", resp.StatusCode),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
var reader io.Reader = resp.Body
|
||||
@@ -88,10 +96,12 @@ func FetchSitemapTool() agent.Tool {
|
||||
resp.Header.Get("Content-Encoding") == "gzip" {
|
||||
gz, err := gzip.NewReader(resp.Body)
|
||||
if err != nil {
|
||||
return agent.ResultJSON(sitemapResult{
|
||||
Found: false,
|
||||
ErrorDetail: fmt.Sprintf("cannot decompress gzipped sitemap: %s", err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
sitemapResult{
|
||||
Found: false,
|
||||
ErrorDetail: fmt.Sprintf("cannot decompress gzipped sitemap: %s", err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
defer func() { _ = gz.Close() }()
|
||||
@@ -104,10 +114,12 @@ func FetchSitemapTool() agent.Tool {
|
||||
|
||||
urls, err := parseSitemapXML(reader)
|
||||
if err != nil {
|
||||
return agent.ResultJSON(sitemapResult{
|
||||
Found: false,
|
||||
ErrorDetail: fmt.Sprintf("cannot parse sitemap XML: %s", err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
sitemapResult{
|
||||
Found: false,
|
||||
ErrorDetail: fmt.Sprintf("cannot parse sitemap XML: %s", err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
result := sitemapResult{
|
||||
|
||||
@@ -80,11 +80,13 @@ func NavigateToURLTool(b *Browser) agent.Tool {
|
||||
return agent.ResultError(b.classifyError(ctx, p.URL, err)), nil
|
||||
}
|
||||
|
||||
return agent.ResultJSON(navigateResult{
|
||||
Title: title,
|
||||
Description: description,
|
||||
FinalURL: finalURL,
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
navigateResult{
|
||||
Title: title,
|
||||
Description: description,
|
||||
FinalURL: finalURL,
|
||||
},
|
||||
), nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -64,10 +64,12 @@ func DiffDocumentsTool() agent.Tool {
|
||||
diff := computeDiff(linesA, linesB, labelA, labelB)
|
||||
|
||||
if diff.tooLarge {
|
||||
return agent.ResultJSON(diffResult{
|
||||
HasDifferences: true,
|
||||
ErrorDetail: diff.output,
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
diffResult{
|
||||
HasDifferences: true,
|
||||
ErrorDetail: diff.output,
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
result := diffResult{
|
||||
|
||||
@@ -65,9 +65,17 @@ func CheckWaybackTool() agent.Tool {
|
||||
var result waybackResult
|
||||
|
||||
// Check availability.
|
||||
availURL := "https://archive.org/wayback/available?url=" + url.QueryEscape(p.URL)
|
||||
availURL, err := url.Parse("https://archive.org/wayback/available")
|
||||
if err != nil {
|
||||
result.ErrorDetail = fmt.Sprintf("cannot parse Wayback Machine URL: %s", err)
|
||||
return agent.ResultJSON(result), nil
|
||||
}
|
||||
|
||||
body, err := httpGet(ctx, client, availURL)
|
||||
q := availURL.Query()
|
||||
q.Set("url", p.URL)
|
||||
availURL.RawQuery = q.Encode()
|
||||
|
||||
body, err := httpGet(ctx, client, availURL.String())
|
||||
if err != nil {
|
||||
result.ErrorDetail = fmt.Sprintf("cannot check Wayback Machine availability: %s", err)
|
||||
return agent.ResultJSON(result), nil
|
||||
|
||||
@@ -68,9 +68,11 @@ func CheckCORSTool() agent.Tool {
|
||||
"Send a CORS preflight (OPTIONS) request to a URL with a given Origin and analyze the Access-Control-* response headers, flagging wildcard origins and origin reflection.",
|
||||
func(ctx context.Context, p corsParams) (agent.ToolResult, error) {
|
||||
if err := netcheck.ValidatePublicURL(p.URL); err != nil {
|
||||
return agent.ResultJSON(corsResult{
|
||||
ErrorDetail: fmt.Sprintf("URL not allowed: %s", err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
corsResult{
|
||||
ErrorDetail: fmt.Sprintf("URL not allowed: %s", err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
@@ -87,9 +89,11 @@ func CheckCORSTool() agent.Tool {
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return agent.ResultJSON(corsResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot build request: %s", err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
corsResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot build request: %s", err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
req.Header.Set("Origin", p.Origin)
|
||||
@@ -97,9 +101,11 @@ func CheckCORSTool() agent.Tool {
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return agent.ResultJSON(corsResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot fetch %s: %s", p.URL, err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
corsResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot fetch %s: %s", p.URL, err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
@@ -81,16 +81,20 @@ func AnalyzeCSPTool() agent.Tool {
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, p.URL, nil)
|
||||
if err != nil {
|
||||
return agent.ResultJSON(cspResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot create request for %s: %s", p.URL, err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
cspResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot create request for %s: %s", p.URL, err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return agent.ResultJSON(cspResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot fetch %s: %s", p.URL, err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
cspResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot fetch %s: %s", p.URL, err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
@@ -73,10 +73,12 @@ func CheckDMARCTool() agent.Tool {
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return agent.ResultJSON(dmarcResult{
|
||||
Found: false,
|
||||
ErrorDetail: fmt.Sprintf("cannot lookup DMARC record: %s", err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
dmarcResult{
|
||||
Found: false,
|
||||
ErrorDetail: fmt.Sprintf("cannot lookup DMARC record: %s", err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
for _, answer := range answers {
|
||||
|
||||
@@ -61,10 +61,12 @@ func CheckDNSSECTool() agent.Tool {
|
||||
withDNSSEC(),
|
||||
)
|
||||
if err != nil {
|
||||
return agent.ResultJSON(dnssecResult{
|
||||
Enabled: false,
|
||||
ErrorDetail: fmt.Sprintf("cannot query DNSKEY records: %s", err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
dnssecResult{
|
||||
Enabled: false,
|
||||
ErrorDetail: fmt.Sprintf("cannot query DNSKEY records: %s", err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -79,9 +80,11 @@ func CheckSecurityHeadersTool() agent.Tool {
|
||||
"Check security-related HTTP headers for a URL (HSTS, CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, Cross-Origin-*-Policy). Also checks if HTTP redirects to HTTPS.",
|
||||
func(ctx context.Context, p headersParams) (agent.ToolResult, error) {
|
||||
if err := netcheck.ValidatePublicURL(p.URL); err != nil {
|
||||
return agent.ResultJSON(headersResult{
|
||||
ErrorDetail: fmt.Sprintf("URL not allowed: %s", err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
headersResult{
|
||||
ErrorDetail: fmt.Sprintf("URL not allowed: %s", err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
@@ -94,11 +97,19 @@ func CheckSecurityHeadersTool() agent.Tool {
|
||||
// First check the HTTP version to detect HTTP→HTTPS redirect.
|
||||
redirectsToHTTPS := false
|
||||
|
||||
httpURL := p.URL
|
||||
if after, ok := strings.CutPrefix(httpURL, "https://"); ok {
|
||||
httpURL = "http://" + after
|
||||
parsedURL, err := url.Parse(p.URL)
|
||||
if err != nil {
|
||||
return agent.ResultJSON(
|
||||
headersResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot parse URL: %s", err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
httpParsed := *parsedURL
|
||||
httpParsed.Scheme = "http"
|
||||
httpURL := httpParsed.String()
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, httpURL, nil)
|
||||
if err == nil {
|
||||
httpResp, err := client.Do(httpReq)
|
||||
@@ -114,25 +125,28 @@ func CheckSecurityHeadersTool() agent.Tool {
|
||||
}
|
||||
|
||||
// Now check the HTTPS version for the actual security headers.
|
||||
httpsURL := p.URL
|
||||
if after, ok := strings.CutPrefix(httpsURL, "http://"); ok {
|
||||
httpsURL = "https://" + after
|
||||
}
|
||||
httpsParsed := *parsedURL
|
||||
httpsParsed.Scheme = "https"
|
||||
httpsURL := httpsParsed.String()
|
||||
|
||||
followClient := &http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
httpsReq, err := http.NewRequestWithContext(ctx, http.MethodGet, httpsURL, nil)
|
||||
if err != nil {
|
||||
return agent.ResultJSON(headersResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot create request for %s: %s", httpsURL, err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
headersResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot create request for %s: %s", httpsURL, err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
resp, err := followClient.Do(httpsReq)
|
||||
if err != nil {
|
||||
return agent.ResultJSON(headersResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot fetch %s: %s", httpsURL, err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
headersResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot fetch %s: %s", httpsURL, err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
@@ -61,34 +61,48 @@ func CheckBreachesTool() agent.Tool {
|
||||
func(ctx context.Context, p hibpParams) (agent.ToolResult, error) {
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
req, err := http.NewRequestWithContext(
|
||||
ctx,
|
||||
http.MethodGet,
|
||||
"https://haveibeenpwned.com/api/v3/breaches?domain="+url.QueryEscape(p.Domain),
|
||||
nil,
|
||||
)
|
||||
hibpURL, err := url.Parse("https://haveibeenpwned.com/api/v3/breaches")
|
||||
if err != nil {
|
||||
return agent.ResultJSON(hibpResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot create request: %s", err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
hibpResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot parse HIBP URL: %s", err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
q := hibpURL.Query()
|
||||
q.Set("domain", p.Domain)
|
||||
hibpURL.RawQuery = q.Encode()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, hibpURL.String(), nil)
|
||||
if err != nil {
|
||||
return agent.ResultJSON(
|
||||
hibpResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot create request: %s", err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "Probo-Vendor-Assessment")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return agent.ResultJSON(hibpResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot fetch breaches: %s", err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
hibpResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot fetch breaches: %s", err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return agent.ResultJSON(hibpResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot read response: %s", err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
hibpResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot read response: %s", err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
@@ -96,23 +110,29 @@ func CheckBreachesTool() agent.Tool {
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return agent.ResultJSON(hibpResult{
|
||||
ErrorDetail: fmt.Sprintf("HIBP API returned status %d", resp.StatusCode),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
hibpResult{
|
||||
ErrorDetail: fmt.Sprintf("HIBP API returned status %d", resp.StatusCode),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
var breaches []breach
|
||||
if err := json.Unmarshal(body, &breaches); err != nil {
|
||||
return agent.ResultJSON(hibpResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot parse response: %s", err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
hibpResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot parse response: %s", err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
return agent.ResultJSON(hibpResult{
|
||||
Found: len(breaches) > 0,
|
||||
Count: len(breaches),
|
||||
Breaches: breaches,
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
hibpResult{
|
||||
Found: len(breaches) > 0,
|
||||
Count: len(breaches),
|
||||
Breaches: breaches,
|
||||
},
|
||||
), nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -77,10 +77,12 @@ func CheckSPFTool() agent.Tool {
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return agent.ResultJSON(spfResult{
|
||||
Found: false,
|
||||
ErrorDetail: fmt.Sprintf("cannot lookup SPF record: %s", err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
spfResult{
|
||||
Found: false,
|
||||
ErrorDetail: fmt.Sprintf("cannot lookup SPF record: %s", err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
var spfRecords []string
|
||||
@@ -100,21 +102,25 @@ func CheckSPFTool() agent.Tool {
|
||||
}
|
||||
|
||||
if len(spfRecords) > 1 {
|
||||
return agent.ResultJSON(spfResult{
|
||||
Found: true,
|
||||
ErrorDetail: fmt.Sprintf("multiple SPF records found (%d); this is an invalid configuration per RFC 7208", len(spfRecords)),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
spfResult{
|
||||
Found: true,
|
||||
ErrorDetail: fmt.Sprintf("multiple SPF records found (%d); this is an invalid configuration per RFC 7208", len(spfRecords)),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
if len(spfRecords) == 1 {
|
||||
record := spfRecords[0]
|
||||
|
||||
return agent.ResultJSON(spfResult{
|
||||
Found: true,
|
||||
RawRecord: record,
|
||||
Policy: parseSPFPolicy(record),
|
||||
Mechanisms: record,
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
spfResult{
|
||||
Found: true,
|
||||
RawRecord: record,
|
||||
Policy: parseSPFPolicy(record),
|
||||
Mechanisms: record,
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
return agent.ResultJSON(spfResult{Found: false}), nil
|
||||
|
||||
@@ -66,10 +66,12 @@ func CheckSSLCertificateTool() agent.Tool {
|
||||
"Check the SSL/TLS certificate for a domain, returning issuer, expiry, protocol version, and validity.",
|
||||
func(ctx context.Context, p sslParams) (agent.ToolResult, error) {
|
||||
if err := netcheck.ValidatePublicDomain(p.Domain); err != nil {
|
||||
return agent.ResultJSON(sslResult{
|
||||
Valid: false,
|
||||
ErrorDetail: fmt.Sprintf("domain not allowed: %s", err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
sslResult{
|
||||
Valid: false,
|
||||
ErrorDetail: fmt.Sprintf("domain not allowed: %s", err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
// This is a certificate inspection tool: we intentionally
|
||||
@@ -96,20 +98,24 @@ func CheckSSLCertificateTool() agent.Tool {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return agent.ResultJSON(sslResult{
|
||||
Valid: false,
|
||||
ErrorDetail: err.Error(),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
sslResult{
|
||||
Valid: false,
|
||||
ErrorDetail: err.Error(),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
state := conn.ConnectionState()
|
||||
if len(state.PeerCertificates) == 0 {
|
||||
return agent.ResultJSON(sslResult{
|
||||
Valid: false,
|
||||
ErrorDetail: "no peer certificates",
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
sslResult{
|
||||
Valid: false,
|
||||
ErrorDetail: "no peer certificates",
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
cert := state.PeerCertificates[0]
|
||||
|
||||
@@ -50,17 +50,21 @@ func CheckWhoisTool() agent.Tool {
|
||||
"Perform a WHOIS lookup on a domain to retrieve registration details including registrar, creation date, expiry date, registrant organization, and name servers.",
|
||||
func(ctx context.Context, p whoisParams) (agent.ToolResult, error) {
|
||||
if err := netcheck.ValidatePublicDomain(p.Domain); err != nil {
|
||||
return agent.ResultJSON(whoisResult{
|
||||
ErrorDetail: fmt.Sprintf("domain not allowed: %s", err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
whoisResult{
|
||||
ErrorDetail: fmt.Sprintf("domain not allowed: %s", err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
// Step 1: query IANA to find the referral WHOIS server.
|
||||
referral, err := queryWhois(ctx, "whois.iana.org:43", p.Domain)
|
||||
if err != nil {
|
||||
return agent.ResultJSON(whoisResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot query IANA WHOIS: %s", err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
whoisResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot query IANA WHOIS: %s", err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
whoisServer := parseWhoisField(referral, "refer")
|
||||
@@ -87,17 +91,21 @@ func CheckWhoisTool() agent.Tool {
|
||||
}
|
||||
|
||||
if err := netcheck.ValidatePublicDomain(whoisHost); err != nil {
|
||||
return agent.ResultJSON(whoisResult{
|
||||
ErrorDetail: fmt.Sprintf("WHOIS referral server not allowed: %s", err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
whoisResult{
|
||||
ErrorDetail: fmt.Sprintf("WHOIS referral server not allowed: %s", err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
// Step 2: query the registrar's WHOIS server.
|
||||
raw, err := queryWhois(ctx, whoisServer, p.Domain)
|
||||
if err != nil {
|
||||
return agent.ResultJSON(whoisResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot query WHOIS server %s: %s", whoisServer, err),
|
||||
}), nil
|
||||
return agent.ResultJSON(
|
||||
whoisResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot query WHOIS server %s: %s", whoisServer, err),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
result := parseWhoisResponse(raw)
|
||||
|
||||
Reference in New Issue
Block a user