@@ -101,6 +101,7 @@ func CheckCORSTool() agent.Tool {
|
||||
ErrorDetail: fmt.Sprintf("cannot fetch %s: %s", p.URL, err),
|
||||
}), nil
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
allowOrigin := resp.Header.Get("Access-Control-Allow-Origin")
|
||||
|
||||
@@ -92,6 +92,7 @@ func AnalyzeCSPTool() agent.Tool {
|
||||
ErrorDetail: fmt.Sprintf("cannot fetch %s: %s", p.URL, err),
|
||||
}), nil
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
raw := resp.Header.Get("Content-Security-Policy")
|
||||
@@ -111,6 +112,7 @@ func AnalyzeCSPTool() agent.Tool {
|
||||
directives := parseCSPDirectives(raw)
|
||||
|
||||
var hasUnsafeEval, hasUnsafeInline, hasWildcard bool
|
||||
|
||||
for _, d := range directives {
|
||||
for _, v := range d.Values {
|
||||
switch v {
|
||||
|
||||
@@ -46,6 +46,7 @@ func parseDMARCTag(record, tag string) string {
|
||||
return after
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -60,6 +61,7 @@ func CheckDMARCTool() agent.Tool {
|
||||
}
|
||||
|
||||
client := dns.NewClient()
|
||||
|
||||
answers, err := queryDNS(
|
||||
ctx,
|
||||
client,
|
||||
|
||||
@@ -53,10 +53,14 @@ func CheckDNSRecordsTool() agent.Tool {
|
||||
|
||||
hdr := dns.Header{Name: fqdn, Class: dns.ClassINET}
|
||||
client := dns.NewClient()
|
||||
var result dnsRecordsResult
|
||||
var errs []string
|
||||
|
||||
var (
|
||||
result dnsRecordsResult
|
||||
errs []string
|
||||
)
|
||||
|
||||
// A records.
|
||||
|
||||
if answers, err := queryDNS(ctx, client, &dns.A{Hdr: hdr}); err != nil {
|
||||
errs = append(errs, fmt.Sprintf("A query failed: %s", err))
|
||||
} else {
|
||||
@@ -148,12 +152,14 @@ func queryDNS(ctx context.Context, client *dns.Client, question dns.RR, opts ...
|
||||
for _, opt := range opts {
|
||||
opt(&msg.MsgHeader)
|
||||
}
|
||||
|
||||
msg.Question = []dns.RR{question}
|
||||
|
||||
resp, _, err := client.Exchange(ctx, msg, "udp", defaultResolverAddr)
|
||||
if err == nil && resp.Truncated {
|
||||
resp, _, err = client.Exchange(ctx, msg, "tcp", defaultResolverAddr)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ func CheckDNSSECTool() agent.Tool {
|
||||
}
|
||||
|
||||
client := dns.NewClient()
|
||||
|
||||
answers, err := queryDNS(
|
||||
ctx,
|
||||
client,
|
||||
@@ -66,8 +67,11 @@ func CheckDNSSECTool() agent.Tool {
|
||||
}), nil
|
||||
}
|
||||
|
||||
var keyCount int
|
||||
var keyDetails []string
|
||||
var (
|
||||
keyCount int
|
||||
keyDetails []string
|
||||
)
|
||||
|
||||
for _, answer := range answers {
|
||||
if key, ok := answer.(*dns.DNSKEY); ok {
|
||||
keyCount++
|
||||
@@ -76,6 +80,7 @@ func CheckDNSSECTool() agent.Tool {
|
||||
if key.Flags&0x0001 != 0 {
|
||||
flags = "KSK"
|
||||
}
|
||||
|
||||
keyDetails = append(
|
||||
keyDetails,
|
||||
fmt.Sprintf("%s (algorithm=%d, flags=%d)", flags, key.Algorithm, key.Flags),
|
||||
|
||||
@@ -52,6 +52,7 @@ type (
|
||||
|
||||
func checkHeader(h http.Header, name string) headerCheck {
|
||||
v := h.Get(name)
|
||||
|
||||
return headerCheck{
|
||||
Present: v != "",
|
||||
Value: v,
|
||||
@@ -92,6 +93,7 @@ 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
|
||||
@@ -118,18 +120,21 @@ func CheckSecurityHeadersTool() agent.Tool {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
resp, err := followClient.Do(httpsReq)
|
||||
if err != nil {
|
||||
return agent.ResultJSON(headersResult{
|
||||
ErrorDetail: fmt.Sprintf("cannot fetch %s: %s", httpsURL, err),
|
||||
}), nil
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
result := headersFromResponse(resp)
|
||||
|
||||
@@ -81,6 +81,7 @@ func CheckBreachesTool() agent.Tool {
|
||||
ErrorDetail: fmt.Sprintf("cannot fetch breaches: %s", err),
|
||||
}), nil
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
|
||||
@@ -26,6 +26,7 @@ func resolverAddr() string {
|
||||
if addr := os.Getenv("DNS_RESOLVER_ADDR"); addr != "" {
|
||||
return addr
|
||||
}
|
||||
|
||||
return "8.8.8.8:53"
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ func CheckSPFTool() agent.Tool {
|
||||
}
|
||||
|
||||
client := dns.NewClient()
|
||||
|
||||
answers, err := queryDNS(
|
||||
ctx,
|
||||
client,
|
||||
@@ -83,6 +84,7 @@ func CheckSPFTool() agent.Tool {
|
||||
}
|
||||
|
||||
var spfRecords []string
|
||||
|
||||
for _, answer := range answers {
|
||||
txt, ok := answer.(*dns.TXT)
|
||||
if !ok {
|
||||
@@ -106,6 +108,7 @@ func CheckSPFTool() agent.Tool {
|
||||
|
||||
if len(spfRecords) == 1 {
|
||||
record := spfRecords[0]
|
||||
|
||||
return agent.ResultJSON(spfResult{
|
||||
Found: true,
|
||||
RawRecord: record,
|
||||
|
||||
@@ -89,16 +89,19 @@ func CheckSSLCertificateTool() agent.Tool {
|
||||
},
|
||||
}
|
||||
netConn, err := dialer.DialContext(ctx, "tcp", p.Domain+":443")
|
||||
|
||||
var conn *tls.Conn
|
||||
if netConn != nil {
|
||||
conn = netConn.(*tls.Conn)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return agent.ResultJSON(sslResult{
|
||||
Valid: false,
|
||||
ErrorDetail: err.Error(),
|
||||
}), nil
|
||||
}
|
||||
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
state := conn.ConnectionState()
|
||||
@@ -124,6 +127,7 @@ func CheckSSLCertificateTool() agent.Tool {
|
||||
for _, ic := range state.PeerCertificates[1:] {
|
||||
opts.Intermediates.AddCert(ic)
|
||||
}
|
||||
|
||||
if _, err := cert.Verify(opts); err != nil {
|
||||
valid = false
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ func CheckWhoisTool() agent.Tool {
|
||||
if whoisServer == "" {
|
||||
whoisServer = parseWhoisField(referral, "whois")
|
||||
}
|
||||
|
||||
if whoisServer == "" {
|
||||
// Try common TLD WHOIS servers as fallback.
|
||||
parts := strings.Split(p.Domain, ".")
|
||||
@@ -84,6 +85,7 @@ func CheckWhoisTool() agent.Tool {
|
||||
if whoisHost == "" {
|
||||
whoisHost = whoisServer
|
||||
}
|
||||
|
||||
if err := netcheck.ValidatePublicDomain(whoisHost); err != nil {
|
||||
return agent.ResultJSON(whoisResult{
|
||||
ErrorDetail: fmt.Sprintf("WHOIS referral server not allowed: %s", err),
|
||||
@@ -114,6 +116,7 @@ func CheckWhoisTool() agent.Tool {
|
||||
years := int(age.Hours() / 24 / 365)
|
||||
months := int(age.Hours()/24/30) % 12
|
||||
result.DomainAge = fmt.Sprintf("%d years, %d months", years, months)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -126,10 +129,12 @@ func CheckWhoisTool() agent.Tool {
|
||||
|
||||
func queryWhois(ctx context.Context, server, domain string) (string, error) {
|
||||
dialer := net.Dialer{Timeout: 10 * time.Second}
|
||||
|
||||
conn, err := dialer.DialContext(ctx, "tcp", server)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot connect to %s: %w", server, err)
|
||||
}
|
||||
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
_ = conn.SetDeadline(time.Now().Add(10 * time.Second))
|
||||
@@ -140,11 +145,13 @@ func queryWhois(ctx context.Context, server, domain string) (string, error) {
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
|
||||
scanner := bufio.NewScanner(conn)
|
||||
for scanner.Scan() {
|
||||
sb.WriteString(scanner.Text())
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return "", fmt.Errorf("cannot read from %s: %w", server, err)
|
||||
}
|
||||
@@ -154,19 +161,23 @@ func queryWhois(ctx context.Context, server, domain string) (string, error) {
|
||||
|
||||
func parseWhoisField(raw, field string) string {
|
||||
field = strings.ToLower(field)
|
||||
|
||||
for line := range strings.SplitSeq(raw, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "%") || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
k, v, ok := strings.Cut(line, ":")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.ToLower(strings.TrimSpace(k)) == field {
|
||||
return strings.TrimSpace(v)
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -199,16 +210,20 @@ var (
|
||||
|
||||
func parseWhoisResponse(raw string) whoisResult {
|
||||
var result whoisResult
|
||||
|
||||
for line := range strings.SplitSeq(raw, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "%") || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
k, v, ok := strings.Cut(line, ":")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
key := strings.ToLower(strings.TrimSpace(k))
|
||||
|
||||
val := strings.TrimSpace(v)
|
||||
if val == "" {
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user