Add browser deploy recon backend with port scan, web crawl, and deploy lane recommendations.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
POST /api/v1/recon/scan probes fleet ports from the server host, crawls owned HTTP targets, maps findings to spread lanes, and records optional oath ledger rows.
This commit is contained in:
219
server/internal/recon/parse.go
Normal file
219
server/internal/recon/parse.go
Normal file
@@ -0,0 +1,219 @@
|
||||
package recon
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
var urlFieldHints = []string{
|
||||
"url", "link", "preview", "webhook", "fetch", "import", "oembed", "image",
|
||||
}
|
||||
|
||||
var cmsPathMarkers = []struct {
|
||||
path string
|
||||
tag string
|
||||
}{
|
||||
{"/wp-admin", "wordpress"},
|
||||
{"/wp-content", "wordpress"},
|
||||
{"/strapi", "strapi"},
|
||||
{"/graphql", "graphql"},
|
||||
{"/admin/login", "admin_login"},
|
||||
}
|
||||
|
||||
// ParseHTML extracts upload forms, URL fields, SSRF score, and CMS hints from HTML.
|
||||
func ParseHTML(pageURL, body string) (fileInputs, multipart []FormFinding, urlFields []URLFieldFinding, ssrfScore int, cms []string) {
|
||||
root, err := html.Parse(strings.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, nil, nil, 0, cmsFromText(pageURL, body)
|
||||
}
|
||||
title := extractTitle(root)
|
||||
_ = title
|
||||
|
||||
var walk func(*html.Node)
|
||||
walk = func(n *html.Node) {
|
||||
if n.Type == html.ElementNode && n.Data == "form" {
|
||||
form := parseForm(pageURL, n)
|
||||
if form.HasFile {
|
||||
fileInputs = append(fileInputs, form)
|
||||
}
|
||||
if form.Multipart {
|
||||
multipart = append(multipart, form)
|
||||
}
|
||||
ssrfScore += scoreForm(form)
|
||||
}
|
||||
if n.Type == html.ElementNode && n.Data == "input" {
|
||||
if field := parseURLField(pageURL, n); field != nil {
|
||||
urlFields = append(urlFields, *field)
|
||||
ssrfScore += 10
|
||||
}
|
||||
}
|
||||
if n.Type == html.ElementNode && (n.Data == "textarea" || n.Data == "select") {
|
||||
if field := parseURLFieldFromNamed(pageURL, attr(n, "name"), attr(n, "id"), attr(n, "placeholder")); field != nil {
|
||||
urlFields = append(urlFields, *field)
|
||||
ssrfScore += 8
|
||||
}
|
||||
}
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
walk(c)
|
||||
}
|
||||
}
|
||||
walk(root)
|
||||
|
||||
cms = cmsFromText(pageURL, body)
|
||||
if ssrfScore > 100 {
|
||||
ssrfScore = 100
|
||||
}
|
||||
return fileInputs, multipart, urlFields, ssrfScore, cms
|
||||
}
|
||||
|
||||
func parseForm(pageURL string, form *html.Node) FormFinding {
|
||||
f := FormFinding{
|
||||
PageURL: pageURL,
|
||||
Action: attr(form, "action"),
|
||||
Method: strings.ToLower(attr(form, "method")),
|
||||
Enctype: strings.ToLower(attr(form, "enctype")),
|
||||
}
|
||||
if f.Method == "" {
|
||||
f.Method = "get"
|
||||
}
|
||||
if strings.Contains(f.Enctype, "multipart") {
|
||||
f.Multipart = true
|
||||
}
|
||||
for c := form.FirstChild; c != nil; c = c.NextSibling {
|
||||
collectFormFields(c, &f)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func collectFormFields(n *html.Node, f *FormFinding) {
|
||||
if n.Type == html.ElementNode {
|
||||
switch n.Data {
|
||||
case "input", "textarea", "select":
|
||||
name := attr(n, "name")
|
||||
if name != "" {
|
||||
f.Fields = append(f.Fields, name)
|
||||
}
|
||||
if n.Data == "input" && strings.EqualFold(attr(n, "type"), "file") {
|
||||
f.HasFile = true
|
||||
f.Multipart = true
|
||||
}
|
||||
if field := parseURLField(f.PageURL, n); field != nil {
|
||||
f.Fields = append(f.Fields, field.Name+"("+field.Hint+")")
|
||||
}
|
||||
}
|
||||
}
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
collectFormFields(c, f)
|
||||
}
|
||||
}
|
||||
|
||||
func parseURLField(pageURL string, n *html.Node) *URLFieldFinding {
|
||||
name := attr(n, "name")
|
||||
id := attr(n, "id")
|
||||
placeholder := attr(n, "placeholder")
|
||||
return parseURLFieldFromNamed(pageURL, name, id, placeholder)
|
||||
}
|
||||
|
||||
func parseURLFieldFromNamed(pageURL, name, id, placeholder string) *URLFieldFinding {
|
||||
joined := strings.ToLower(strings.Join([]string{name, id, placeholder}, " "))
|
||||
for _, hint := range urlFieldHints {
|
||||
if strings.Contains(joined, hint) {
|
||||
label := name
|
||||
if label == "" {
|
||||
label = id
|
||||
}
|
||||
return &URLFieldFinding{
|
||||
PageURL: pageURL,
|
||||
Name: label,
|
||||
Type: "text",
|
||||
Hint: hint,
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func scoreForm(f FormFinding) int {
|
||||
score := 0
|
||||
action := strings.ToLower(f.Action)
|
||||
for _, hint := range urlFieldHints {
|
||||
if strings.Contains(action, hint) {
|
||||
score += 15
|
||||
}
|
||||
}
|
||||
for _, field := range f.Fields {
|
||||
lower := strings.ToLower(field)
|
||||
for _, hint := range urlFieldHints {
|
||||
if strings.Contains(lower, hint) {
|
||||
score += 5
|
||||
}
|
||||
}
|
||||
}
|
||||
if f.HasFile {
|
||||
score += 5
|
||||
}
|
||||
return score
|
||||
}
|
||||
|
||||
func cmsFromText(pageURL, body string) []string {
|
||||
seen := map[string]bool{}
|
||||
var out []string
|
||||
lowerURL := strings.ToLower(pageURL)
|
||||
lowerBody := strings.ToLower(body)
|
||||
add := func(tag string) {
|
||||
if tag == "" || seen[tag] {
|
||||
return
|
||||
}
|
||||
seen[tag] = true
|
||||
out = append(out, tag)
|
||||
}
|
||||
for _, marker := range cmsPathMarkers {
|
||||
if strings.Contains(lowerURL, marker.path) || strings.Contains(lowerBody, marker.path) {
|
||||
add(marker.tag)
|
||||
}
|
||||
}
|
||||
if strings.Contains(lowerBody, "strapi") {
|
||||
add("strapi")
|
||||
}
|
||||
if strings.Contains(lowerBody, "wp-content") || strings.Contains(lowerBody, "wordpress") {
|
||||
add("wordpress")
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func extractTitle(root *html.Node) string {
|
||||
var title string
|
||||
var walk func(*html.Node)
|
||||
walk = func(n *html.Node) {
|
||||
if n.Type == html.ElementNode && n.Data == "title" && n.FirstChild != nil {
|
||||
title = strings.TrimSpace(n.FirstChild.Data)
|
||||
return
|
||||
}
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
walk(c)
|
||||
}
|
||||
}
|
||||
walk(root)
|
||||
return title
|
||||
}
|
||||
|
||||
func attr(n *html.Node, key string) string {
|
||||
for _, a := range n.Attr {
|
||||
if a.Key == key {
|
||||
return a.Val
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func readBodyLimited(r io.Reader, max int64) (string, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
_, err := io.Copy(buf, io.LimitReader(r, max))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
Reference in New Issue
Block a user