Files
openserp/core/fpcheck/detectors/pixelscan.go
2026-04-23 00:53:30 +03:00

205 lines
6.0 KiB
Go

package detectors
import (
"context"
"fmt"
"strings"
"time"
"github.com/go-rod/rod"
"github.com/karust/openserp/core/fpcheck"
)
const (
pixelscanURL = "https://pixelscan.net/bot-check"
)
type PixelScan struct{}
func NewPixelScan() fpcheck.Detector {
return PixelScan{}
}
func (PixelScan) Name() string {
return "pixelscan"
}
func (PixelScan) URL() string {
return pixelscanURL
}
func (PixelScan) Extract(ctx context.Context, page *rod.Page) (map[string]fpcheck.Detection, string, error) {
err := waitFor(ctx, 25*time.Second, 250*time.Millisecond, func() (bool, error) {
res, err := page.Eval(`() => {
const hasSummary = !!document.querySelector(".bot-check-summary, .bot-check__summary, .bot-check-accordion__row");
const hasState = !!document.querySelector(".state-success, .state-error");
const body = (document.body && document.body.innerText ? document.body.innerText : "").toLowerCase();
return hasSummary || hasState || body.includes("running bot detection") || body.includes("definitely a human") || body.includes("bot behavior detected");
}`)
if err != nil {
return false, nil
}
var ready bool
if err := res.Value.Unmarshal(&ready); err != nil {
return false, nil
}
return ready, nil
})
if err != nil {
return nil, "", fmt.Errorf("pixelscan readiness: %w", err)
}
res, err := page.Eval(`() => {
const normalize = (value) => (value || "").replace(/\s+/g, " ").trim();
const isVisible = (node) => !!node && !!(node.offsetParent || node.getClientRects().length) &&
window.getComputedStyle(node).display !== "none" &&
window.getComputedStyle(node).visibility !== "hidden" &&
window.getComputedStyle(node).opacity !== "0";
const successNode = document.querySelector(".state-success");
const errorNode = document.querySelector(".state-error");
let state = "unknown";
if (isVisible(successNode)) state = "human";
if (isVisible(errorNode)) state = "bot";
if (state === "unknown") {
const text = normalize(document.body && document.body.innerText ? document.body.innerText : "").toLowerCase();
if (text.includes("you're definitely a human")) state = "human";
if (text.includes("bot behavior detected")) state = "bot";
}
const summary = [];
for (const section of Array.from(document.querySelectorAll(".summary-section"))) {
const full = normalize(section.innerText || section.textContent || "");
if (!full) continue;
const statusNode = section.querySelector(".summary-section__status");
const status = normalize(statusNode ? (statusNode.innerText || statusNode.textContent || "") : "");
let name = full;
if (status) {
name = normalize(full.replace(new RegExp("\\\\s*" + status + "\\\\s*\\\\d*\\\\s*parameters?$", "i"), ""));
}
if (!name || !status) continue;
summary.push({name, status});
}
const rows = [];
for (const row of Array.from(document.querySelectorAll(".bot-check-accordion__row"))) {
const statusNode = row.querySelector(".bot-check-accordion__status");
const labelNode = row.querySelector(".bot-check-accordion__label");
const status = normalize(statusNode ? (statusNode.innerText || statusNode.textContent || "") : "");
if (!status) continue;
let name = normalize(labelNode ? (labelNode.innerText || labelNode.textContent || "") : "");
if (!name) {
const full = normalize(row.innerText || row.textContent || "");
name = normalize(full.replace(new RegExp("\\\\s*" + status + "\\\\s*$", "i"), ""));
}
if (!name) continue;
rows.push({name, status});
}
const body = normalize(document.body && document.body.innerText ? document.body.innerText : "");
return {
state,
summary,
rows,
body: body.slice(0, 6000),
};
}`)
if err != nil {
return nil, "", err
}
var payload struct {
State string `json:"state"`
Summary []struct {
Name string `json:"name"`
Status string `json:"status"`
} `json:"summary"`
Rows []struct {
Name string `json:"name"`
Status string `json:"status"`
} `json:"rows"`
Body string `json:"body"`
}
if err := res.Value.Unmarshal(&payload); err != nil {
return nil, "", fmt.Errorf("decode pixelscan payload: %w", err)
}
payload.Body = strings.TrimSpace(payload.Body)
if payload.Body == "" {
return nil, "", fmt.Errorf("pixelscan body is empty")
}
detections := make(map[string]fpcheck.Detection)
if payload.State != "" {
overallDetected := strings.EqualFold(strings.TrimSpace(payload.State), "bot")
overallStatus := strings.TrimSpace(payload.State)
if overallStatus == "" {
overallStatus = "unknown"
}
overallSeverity := ""
if overallDetected {
overallSeverity = "critical"
}
detections["overall_verdict"] = fpcheck.Detection{
Detected: overallDetected,
Description: overallStatus,
Severity: overallSeverity,
}
}
for _, item := range payload.Summary {
key := "summary_" + normalizeKey(item.Name)
if key == "summary_unknown" {
continue
}
detected := classifyStatus(item.Status)
severity := ""
if detected && hasKeyword(strings.ToLower(item.Name), []string{"webdriver", "cdp", "bot"}) {
severity = "critical"
}
detections[key] = fpcheck.Detection{
Detected: detected,
Description: strings.TrimSpace(item.Status),
Severity: severity,
}
}
for _, row := range payload.Rows {
key := normalizeKey(row.Name)
if key == "unknown" {
continue
}
detected := classifyStatus(row.Status)
severity := ""
if detected && hasKeyword(strings.ToLower(row.Name), []string{"webdriver", "cdp", "headless", "automation"}) {
severity = "critical"
}
detections[key] = fpcheck.Detection{
Detected: detected,
Description: strings.TrimSpace(row.Status),
Severity: severity,
}
}
if len(detections) == 0 {
if score, ok := extractScore(payload.Body); ok {
scoreValue := score
detections["overall_score"] = fpcheck.Detection{
Detected: false,
Description: fmt.Sprintf("score %.2f", score),
Numeric: &scoreValue,
}
}
}
if len(detections) == 0 {
return nil, payload.Body, fmt.Errorf("pixelscan detections not found")
}
return detections, payload.Body, nil
}