mirror of
https://github.com/karust/openserp.git
synced 2026-08-05 16:53:54 +08:00
242 lines
5.6 KiB
Go
242 lines
5.6 KiB
Go
package detectors
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-rod/rod"
|
|
"github.com/karust/openserp/core/fpcheck"
|
|
)
|
|
|
|
var (
|
|
reNonAlphanumUnderscore = regexp.MustCompile(`[^a-z0-9_]+`)
|
|
reMultiUnderscore = regexp.MustCompile(`_+`)
|
|
reExtractScore = regexp.MustCompile(`(?i)(score|overall|risk)[^\d]{0,20}(\d+(?:\.\d+)?)`)
|
|
)
|
|
|
|
type detectorRow struct {
|
|
Name string `json:"name"`
|
|
Status string `json:"status"`
|
|
Detail string `json:"detail"`
|
|
}
|
|
|
|
func waitFor(ctx context.Context, timeout time.Duration, poll time.Duration, probe func() (bool, error)) error {
|
|
if timeout <= 0 {
|
|
timeout = 20 * time.Second
|
|
}
|
|
if poll <= 0 {
|
|
poll = 250 * time.Millisecond
|
|
}
|
|
|
|
deadline := time.Now().Add(timeout)
|
|
for time.Now().Before(deadline) {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
|
|
ok, err := probe()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ok {
|
|
return nil
|
|
}
|
|
|
|
timer := time.NewTimer(poll)
|
|
select {
|
|
case <-timer.C:
|
|
case <-ctx.Done():
|
|
timer.Stop()
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("ready condition not met after %s", timeout)
|
|
}
|
|
|
|
func normalizeKey(name string) string {
|
|
name = strings.ToLower(strings.TrimSpace(name))
|
|
if name == "" {
|
|
return "unknown"
|
|
}
|
|
|
|
replacer := strings.NewReplacer(
|
|
" ", "_",
|
|
"-", "_",
|
|
"/", "_",
|
|
"\\", "_",
|
|
":", "_",
|
|
".", "_",
|
|
)
|
|
name = replacer.Replace(name)
|
|
name = reNonAlphanumUnderscore.ReplaceAllString(name, "")
|
|
name = reMultiUnderscore.ReplaceAllString(name, "_")
|
|
name = strings.Trim(name, "_")
|
|
if name == "" {
|
|
return "unknown"
|
|
}
|
|
return name
|
|
}
|
|
|
|
func classifyStatus(status string) bool {
|
|
value := strings.ToLower(strings.TrimSpace(status))
|
|
if value == "" {
|
|
return false
|
|
}
|
|
|
|
notDetected := []string{"not detected", "not found", "clean", "clear", "pass", "passed", "ok", "safe", "green", "false", "no"}
|
|
for _, marker := range notDetected {
|
|
if strings.Contains(value, marker) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
detected := []string{"detected", "fail", "failed", "bot", "leak", "warning", "critical", "red", "true", "yes"}
|
|
for _, marker := range detected {
|
|
if strings.Contains(value, marker) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func parseRows(page *rod.Page) ([]detectorRow, error) {
|
|
res, err := page.Eval(`() => {
|
|
const normalize = (value) => (value || "").replace(/\s+/g, " ").trim();
|
|
const rows = [];
|
|
const seen = new Set();
|
|
|
|
const tableRows = Array.from(document.querySelectorAll("table tr"));
|
|
for (const row of tableRows) {
|
|
const cells = Array.from(row.querySelectorAll("th, td"));
|
|
if (cells.length < 2) continue;
|
|
const name = normalize(cells[0].innerText || cells[0].textContent || "");
|
|
if (!name) continue;
|
|
if (/^(test(\s+name)?|property|status|result|check)$/i.test(name)) continue;
|
|
|
|
const valueCell = cells[cells.length - 1];
|
|
const status = normalize(valueCell.innerText || valueCell.textContent || "");
|
|
if (!status) continue;
|
|
|
|
const key = name.toLowerCase();
|
|
if (seen.has(key)) continue;
|
|
seen.add(key);
|
|
|
|
rows.push({
|
|
name,
|
|
status,
|
|
detail: normalize(row.innerText || row.textContent || ""),
|
|
});
|
|
}
|
|
|
|
const candidates = Array.from(document.querySelectorAll("[data-test], [data-testid], [data-check], [data-name], .check, .result, li"));
|
|
for (const node of candidates) {
|
|
const text = normalize(node.innerText || node.textContent || "");
|
|
if (!text || text.length > 260) continue;
|
|
if (!/(pass|fail|detected|not detected|warning|critical|true|false|yes|no|leak|bot)/i.test(text)) continue;
|
|
|
|
let name = normalize(node.getAttribute("data-check") || node.getAttribute("data-name") || node.getAttribute("data-testid") || "");
|
|
let status = "";
|
|
|
|
if (!name) {
|
|
const parts = text.split(/[:\-|]/).map(normalize).filter(Boolean);
|
|
if (parts.length >= 2) {
|
|
name = parts[0];
|
|
status = parts.slice(1).join(" ");
|
|
}
|
|
}
|
|
|
|
if (!name) {
|
|
const lines = text.split(/\n+/).map(normalize).filter(Boolean);
|
|
if (lines.length >= 2) {
|
|
name = lines[0];
|
|
status = lines.slice(1).join(" ");
|
|
}
|
|
}
|
|
|
|
if (!name) continue;
|
|
if (!status) {
|
|
status = text;
|
|
}
|
|
|
|
const key = name.toLowerCase();
|
|
if (seen.has(key)) continue;
|
|
seen.add(key);
|
|
|
|
rows.push({ name, status, detail: text });
|
|
}
|
|
|
|
return rows;
|
|
}`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var rows []detectorRow
|
|
if err := res.Value.Unmarshal(&rows); err != nil {
|
|
return nil, fmt.Errorf("decode detector rows: %w", err)
|
|
}
|
|
return rows, nil
|
|
}
|
|
|
|
func rowsToDetections(rows []detectorRow, criticalKeywords []string) map[string]fpcheck.Detection {
|
|
out := make(map[string]fpcheck.Detection, len(rows))
|
|
for _, row := range rows {
|
|
key := normalizeKey(row.Name)
|
|
if key == "unknown" {
|
|
continue
|
|
}
|
|
|
|
detected := classifyStatus(row.Status)
|
|
severity := ""
|
|
if detected && hasKeyword(key+" "+strings.ToLower(row.Detail), criticalKeywords) {
|
|
severity = "critical"
|
|
}
|
|
|
|
description := strings.TrimSpace(row.Status)
|
|
if description == "" {
|
|
description = strings.TrimSpace(row.Detail)
|
|
}
|
|
|
|
out[key] = fpcheck.Detection{
|
|
Detected: detected,
|
|
Description: description,
|
|
Severity: severity,
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func hasKeyword(value string, keywords []string) bool {
|
|
if len(keywords) == 0 {
|
|
return false
|
|
}
|
|
for _, keyword := range keywords {
|
|
k := strings.ToLower(strings.TrimSpace(keyword))
|
|
if k == "" {
|
|
continue
|
|
}
|
|
if strings.Contains(value, k) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func extractScore(text string) (float64, bool) {
|
|
matches := reExtractScore.FindStringSubmatch(text)
|
|
if len(matches) < 3 {
|
|
return 0, false
|
|
}
|
|
score, err := strconv.ParseFloat(matches[2], 64)
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
return score, true
|
|
}
|