Side-loadable Go POC Development

Write vulnerability templates in pure Go (interpreted in-process by the scanner, no Go toolchain needed): @meta header and every injected scan symbol.

This document is for third-party developers: when a YAML template cannot express your detection logic, a single pure-Go source file plus a @meta comment header is all you need to write a vulnerability template that scan tasks can select. The scanner node executes it in-process with a built-in Go interpreter, and the target host does not need a Go toolchain — drop the file into the load directory and it takes effect (hot-load).

提示

This document is organized as a function-by-function deep dive. Chapter 5 is the core: every public symbol of the injected scan package has its own level-4 section, each presenting "purpose → signature → parameter table → returns (a field table for structs) → a ready-to-paste code snippet → notes". All signatures are copied verbatim from the authoritative symbol table injected by the platform; no symbol outside that table exists.


1. What It Is / When to Choose It

1.1 Definition

A hot-loadable Go POC is one form of vulnerability template, not a plugin. The form is determined by the template's PocType field:

  • PocType="yaml" — declarative YAML template handled by the scanner node's native engine;
  • PocType="go" — the subject of this document: pure Go source, interpreted in-process;
  • PocType="wasm" — WASM binary template running on the wazero runtime (signature required).

It is made of exactly two things:

  1. A // @meta:Key=Value comment header at the top of the file (not YAML — these are comments);
  2. A standard Go program: package main + func main(), whose capabilities are all accessed through import "scan", the symbol package injected by the host.

The interpreter invokes main() automatically; you do not need to — and cannot — call main.main() explicitly.

1.2 Storage and Encryption: Plaintext on the Controller, Ciphertext on the Node

The same Go POC has different on-disk forms at the two ends; this is the key to understanding the whole pipeline:

LocationDirectory and file nameContentNotes
Controllerpoc/go/<VulnIde>.goPlaintext Go sourceScanned and loaded when the controller starts; the GUI editor and GetGoPocDetail both read it
Scanner nodescan-poc/go/<VulnIde>.gopocAES-GCM ciphertextAfter the controller delivers it to the node, the node encrypts it with the global key and saves it into its own run directory
说明

Legacy stores were organized by language as scan-poc/<lang>/go/<VulnIde>.gopoc; after the multilingual single-file change on 2026-09-27, Go POCs — like YAML POCs — are no longer placed in per-language directories. The current path is always scan-poc/go/<VulnIde>.gopoc.

  • The node-side extension is .gopoc, not .go. This is a hard-won lesson: the ciphertext bytes contain no //go:build ignore, so the Go toolchain treats scan-poc/go/<VulnIde>.go as source and go build ./... fails immediately with illegal character U+00B0.
  • On startup the node automatically renames legacy .go ciphertext files to .gopoc (best-effort); genuine source files carrying //go:build ignore are never mis-renamed.
  • When reading, the node first attempts AES-GCM decryption and falls back to plaintext on failure, so a plaintext .gopoc file placed locally also runs (which is convenient for debugging).

1.3 Comparison with YAML / WASM

重要

If it can be done in YAML, always use YAML. The 200 built-in generic vulnerabilities have already been migrated from Go POCs to YAML templates. Only complex algorithmic scenarios that the declarative YAML syntax cannot express are worth a hot-loadable Go POC.

DimensionYAML templateHot-loadable GoWASM template
ExecutionNative Go engine, unmarshalled only onceEvery run goes through the interpreter (compilation + reflection)wazero runtime
PerformanceFastestAn order of magnitude slower than YAMLModerate (compiled once, reusable)
ExpressivenessDeclarative: request sequences + matching + regex + expressions + timing + reverse connectionTuring-complete; arbitrary loops, parsing and algorithmsTuring-complete, cross-language
DistributionSingle fileSingle file (plaintext on the controller / ciphertext on the node)Binary + signature
Maintenance costLowHighHigh
Barrier to entryNoneBasic Go knowledge; no toolchain neededBuild chain + signature
Signature controlNone (fingerprint sync)None (fingerprint sync)Hard gate: unsigned templates never run
RecommendationFirst choiceOnly for complex algorithms or custom parsingFor binary distribution or strong signing requirements

Typical scenarios that call for Go: implementing your own signature verification, custom serialization, multi-round state machines, bit-level parsing of binary protocols, dynamically assembling unconventional request bodies, and so on. If it is merely "request + regex match", go back and write YAML.


2. Source Skeleton and the @meta Comment Header

2.1 //go:build ignore Must Be the First Line, and It Is Not Optional

//go:build ignore

// @meta:Name=示例
package main
...

//go:build ignore must be the first line of the file (immediately followed by a blank line). The reason: these plaintext templates live in the project directory alongside the source tree, and without the build constraint go build ./... and go vet ./... would compile them as ordinary source — which either fails the build or packs scripts you should never compile into your binary. With ignore in place the Go toolchain skips them, while the interpreter ignores build tags and interprets them all the same.

Skeletons generated by the controller from metadata automatically carry this line; never omit it when creating a file by hand.

2.2 @meta Syntax and the Parsing Regex

The controller and the scanner node parse @meta with the same regex (identical behavior at both ends):

var metaLinePattern = regexp.MustCompile(
  `(?m)^\s*//\s*@meta:([A-Za-z]\w*)(?:\.([A-Za-z0-9_-]{1,16}))?(\+)?\s*=\s*(.*?)\s*$`)

Breaking it down (just enough to get by — no need to memorize):

  • ^ with (?m): each line is matched independently, and the comment must be at the start of a line (leading indentation is allowed); a // @meta: in the middle of a line is not recognized;
  • //\s*@meta:: whitespace is allowed between // and @meta:;
  • ([A-Za-z]\w*): the key name, which must start with a letter and may be followed by letters, digits or underscores;
  • (?:\.([A-Za-z0-9_-]{1,16}))?: an optional language suffix such as .en;
  • (\+)?: the optional multi-line continuation marker;
  • \s*=\s*(.*?)\s*$: the equals sign; the value extends to the end of the line with leading and trailing whitespace trimmed.

Four forms:

FormMeaning
// @meta:Name=SQL 注入检测Ordinary key-value
// @meta:Description+=第二行内容Multi-line continuation: appended to the key's existing value with \n
// @meta:Name.en=SQL Injection DetectionLanguage suffix; recognized for language-related keys only
// @meta:DefaultLanguage=cnBase language; defaults to cn
注意

Key names are case-sensitive and must be spelled exactly. Writing @Meta:, @ meta: or meta: fails silently, and the symptom is "the vulnerability name is empty". Whitespace on either side of the value separator = is trimmed automatically.

2.3 Full Table of Supported Keys

The template metadata keys the @meta comment header can map to are listed below (taken one by one):

KeyTypeRequiredDefaultExample valueConsequence of a mistake
NamestringYes (or VulnName)emptySQL 注入检测The vulnerability name is empty and a blank entry appears in the list
VulnNamestringNoemptySQL 注入检测Alias of Name only; if both are missing the name is empty
CVEIdstringNoemptyCVE-2024-0001The card shows no CVE number
CweIdstringNoemptyCWE-89The card shows no CWE classification
CvssScorestringNoempty9.8The card shows no CVSS score
CvssVectorstringNoemptyCVSS:3.1/AV:N/...The card shows no vector string
LevelstringNoemptyhighAn empty or misspelled value is reported as low severity
Descriptionstring (language-related)Noempty检测 SQL 报错特征The card's "Details" is empty
Solutionstring (language-related)Noempty使用参数化查询The card's "Remediation" is empty
AuthorstringNoemptyadminThe author field is empty
ReferencesstringNoemptyhttps://example.com/advisoryThe reference links are empty (use += for multiple lines)
FingerprintstringNoemptyexample-productThe fingerprint is empty
AffectedProductsstring (language-related)NoemptyExample Product 1.0The affected products are empty
VerificationstringNoempty手动复现步骤The verification notes are empty
TestDepthstringNopackpack/single-dir/single-domainAn empty value falls back to pack
ConfidenceintNo8090An invalid value, or one ≤ 0, uses 80
EnabledboolNotruefalseThe template is disabled when the value is false (case-insensitive)
DefaultLanguagestringNocncnAn empty value falls back to the platform default base language cn

A comment header with every key written out looks like this (for reference only; optional keys may be omitted):

//go:build ignore

// @meta:Name=综合示例
// @meta:VulnName=综合示例
// @meta:Name.en=Comprehensive Example
// @meta:CVEId=CVE-2024-0001
// @meta:CweId=CWE-89
// @meta:CvssScore=9.8
// @meta:CvssVector=CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
// @meta:Level=critical
// @meta:Description=示例描述
// @meta:Description.en=Example description
// @meta:Solution=示例修复建议
// @meta:Solution.en=Example fix
// @meta:Author=admin
// @meta:References=https://example.com/advisory
// @meta:Fingerprint=example-product
// @meta:AffectedProducts=Example Product 1.0
// @meta:AffectedProducts.en=Example Product 1.0
// @meta:Verification=手动复现步骤
// @meta:TestDepth=pack
// @meta:Confidence=90
// @meta:Enabled=true
// @meta:DefaultLanguage=cn

package main

There are only 4 language-related keys: Name, Description, Solution and AffectedProducts. Only these support the key.language-code suffix.

2.4 Multilingual Aggregation Rules

警告

Four-end language-pack synchronization is a hard requirement for product UI and log text; what follows concerns POC template text, which is a different system — do not conflate the two. POC template multilingual support uses the Languages mechanism described in this section.

  • Keys without a suffix go into the base table and are then placed, as the text of the base language, into Languages[DefaultLanguage];
  • Keys with a language suffix (Name.en and so on) go into langs[language-code], each forming its own block;
  • Finally the flat fields are backfilled with the base-language text (exactly the same semantics as YAML POCs).
// @meta:Name=SQL 注入检测
// @meta:Name.en=SQL Injection Detection
// @meta:Description=检测 SQL 报错特征
// @meta:Description.en=Detect SQL error patterns
// @meta:DefaultLanguage=cn

The snippet above produces two complete language blocks: Languages["cn"] and Languages["en"]. New templates must provide both cn and en, otherwise Chinese text appears in the English UI.

If a non-language-related key is given a suffix (such as References.en=...), the suffix is not recognized and the key name is kept as References.en, which means the value is lost — do not write it that way.

2.5 Complete, Copy-Ready POC Source Skeleton

The skeleton below contains //go:build ignore, a full @meta header, package main and func main(); create a new file, paste it in, and adapt it:

//go:build ignore

// @meta:Name=示例漏洞
// @meta:Name.en=Example Vulnerability
// @meta:VulnName=示例漏洞
// @meta:CVEId=CVE-2024-0001
// @meta:CweId=CWE-89
// @meta:CvssScore=7.5
// @meta:Level=medium
// @meta:Description=响应中出现敏感特征
// @meta:Description.en=Sensitive marker found in response
// @meta:Solution=移除敏感信息
// @meta:Solution.en=Remove the sensitive information
// @meta:Author=admin
// @meta:References=https://example.com/advisory
// @meta:Fingerprint=example-product
// @meta:AffectedProducts=Example Product 1.0
// @meta:AffectedProducts.en=Example Product 1.0
// @meta:Verification=手动复现步骤
// @meta:TestDepth=pack
// @meta:Confidence=85
// @meta:Enabled=true
// @meta:DefaultLanguage=cn

package main

import (
	"scan"
)

func main() {
	// 1) 打印调试日志(GUI 调试面板可见)
	scan.Log("开始检测: " + scan.Flow.Method + " " + scan.Flow.URL)

	// 2) 发起请求(自动携带 Cookie 会话)
	resp := scan.HTTPGet(scan.Flow.URL)
	if resp.Error != "" {
		scan.Log("请求失败: " + resp.Error)
		return
	}
	if resp.StatusCode != 200 {
		scan.Log("状态码非 200,跳过: " + scan.JSONDump(resp.StatusCode))
		return
	}

	// 3) 判定命中
	if scan.Contains(scan.ToLower(resp.Body), "sensitive_marker") {
		// 4) 回传一条漏洞发现
		scan.Report(scan.VulnFinding{
			Name:     "示例漏洞",
			Detail:   "响应体命中敏感特征",
			Evidence: scan.Substr(resp.Body, 0, 200),
			Level:    "medium",
			URL:      resp.URL,
			Response: resp.RawHeaders + "\n\n" + scan.Substr(resp.Body, 0, 500),
		})
	}
}
提示

Treat this skeleton as a template to copy: change the name, severity and languages in the @meta header, then replace the decision logic in main() with your own detection algorithm. Every symbol in Chapter 5 can be pasted into main() on its own.

2.6 Line-by-Line Walkthrough of the Minimal Runnable Skeleton

LinePurposeConsequence of a mistake
//go:build ignoreMakes go build ./... skip the fileA build error, or the file is compiled into the binary
// @meta:Name=示例漏洞Vulnerability name (required)The name is empty and the list shows a blank entry
// @meta:Name.en=...English nameThe English UI shows Chinese
// @meta:DefaultLanguage=cnBase languageFalls back to cn
package mainMarks a valid Go source fileSilently skipped at load time (the template "disappears")
import "scan"Gets all injected symbolsWithout the import, every scan. reference is undefined
func main()The only entry point, executed automatically by the interpreterWithout main() the code never runs

3. Where the Entry Point Is: from the POC Template List to func main() Being Executed

This chapter answers the question third-party developers ask most often: "Who invokes this code I wrote, at which step, and how?"

3.1 Step by Step Through the Whole Chain

  1. In the GUI's "Add Vulnerability" dialog, choose type Go (the tab is labeled "Go template"), paste the source into the editor and save; alternatively, the AI penetration agent submits it through the generate_poc tool. POCs submitted by AI are forced to disabled (Enabled=false) and tagged with their author source; they require manual review before being enabled.
  2. The controller persists the plaintext source: on save the controller writes it to poc/go/<VulnIde>.go under its run directory; if only metadata was filled in without source, a skeleton with a @meta header and package main is generated from the metadata before writing, and the template is registered in the in-memory template list.
  3. Controller startup / hot-load registration: at startup the controller ensures the poc/go directory exists, then scans the *.go files under it: files without a package declaration are skipped; for the rest the @meta header is parsed into template metadata (type marked as Go), multilingual fields are normalized, and the template is registered in the template list.
  4. Task delivery to the scanner node: the controller delivers the selected template (including the source text) to the scanner node over TCP. The node recognizes it as a Go POC by its package declaration or @meta: header, encrypts it with the global AES key and writes it to scan-poc/go/<VulnIde>.gopoc (ciphertext), registering it in its in-memory template table (Go POCs do not take part in YAML fingerprint sync).
  5. Loading local ciphertext on node startup/reload: at startup the node ensures the scan-poc/go directory exists and first renames historical .go ciphertext files to .gopoc; as it reads each local file it tries AES-GCM decryption first and treats a failure as plaintext, accepting the file only when a package declaration can be parsed, and then registers it in the local template table after parsing @meta.
  6. Handing the source to the interpreter at task execution time: when a scan selects a template with PocType == "go", the node builds the run environment for this execution (target flow, timeout, proxy, etc.) and hands your source to the interpreter.
  7. The interpreter automatically executes func main(): the Go standard library and the platform's own scan symbol package are pre-registered in the interpreter, which then interprets your source in a controlled goroutine. For source containing func main(), the interpreter runs main automatically; there is no need to call main.main() explicitly.
  8. Your code calls host symbols through import "scan": scan.Flow / scan.HTTP() / scan.Report() / scan.Log() and the rest are all injected by the platform before execution; you only call them with the signatures in Chapter 5.
  9. scan.Report returns findings: the node collects every finding from this run, assembles each one into a vulnerability detail and reports it to the controller for storage; logs and the HTTP request count are returned at the same time.

3.2 The Entry Point Is Just func main()

  • The only entry point is func main(): there is no _start, no exported function, and no callback to register. Once the interpreter receives source containing func main(), it calls it automatically while interpreting.
  • Requests you write sequentially inside main() execute sequentially within the same run; the shared Cookie session (Chapter 5 / Chapter 6) naturally chains "log in → access" together.
  • When main() returns, this POC run ends; the collected findings and logs are handed back to the node along with the execution result.

3.3 Available Range of the Standard Library

The execution engine has the Go standard library pre-registered, so a POC may import Go standard library packages, for example:

import (
	"scan"
	"strings"
	"strconv"
	"encoding/json"
	"fmt"
)

Commonly available packages: fmt, strings, strconv, bytes, regexp, encoding/json, encoding/base64, encoding/hex, net/url, net/http, crypto/*, time, sort, math and so on.

注意

You may only import "scan" and already-registered standard library packages. Third-party packages (such as github.com/...) are not registered, and importing them makes interpretation fail. In the vast majority of cases the scan package (Chapter 5) is sufficient; there is no need to pull in third-party code.

3.4 Origin of the .gopoc Extension

The node stores ciphertext locally under the .gopoc extension. It must not be .go: an earlier implementation wrote AES ciphertext to scan-poc/go/<VulnIde>.go; the ciphertext contains no //go:build ignore, so the Go toolchain compiled the file as source and the node project's go build ./... failed with illegal character U+00B0. After the fix: local ciphertext always uses .gopoc, and historical .go ciphertext files are renamed automatically when the node starts.


4. Loading and Execution Pipeline

4.1 Controller Side: Scan Directory → Parse @meta → Register

On controller startup / hot-load:
  1. Ensure that poc/go exists under the run directory
  2. Walk poc/go/*.go and read each source file
  3. The source must contain a package declaration, otherwise it is silently skipped
  4. VulnIde = file name without its extension; parse the @meta header for metadata
  5. Normalize multilingual fields and register in the template list (type marked as Go)

Key points:

  • Files without a package declaration are silently skipped — no error is raised, the template simply "goes missing";
  • VulnIde is taken directly from the file name. Duplicate file names overwrite each other, so make sure names are globally unique;
  • This scan runs once at controller startup; templates saved later are written to disk and registered in memory by the controller directly.

4.2 Scanner Node Side: Migration → Loading → Decryption

On node startup / reload:
  1. Ensure the go subdirectory exists under the local scan directory
  2. Rename historical .go ciphertext files to .gopoc (best-effort)
  3. Walk *.gopoc while tolerating historical *.go files, reading them one by one
  4. Try AES-GCM decryption first, treat a failure as plaintext
  5. Accept the file only if a package declaration can be parsed; register by VulnIde
     (deduplicate by name, newer extension wins)

4.3 Execution: In-Process Interpretation

说明

How it works: the hot-load execution engine is based on a built-in Go interpreter (an MIT-licensed open-source component), and the scanner node interprets your source in-process, which is why you write standard Go syntax and the target host needs no Go toolchain. This is an ordinary technology choice (it was chosen because it fully supports Go syntax and can have the platform's own scan symbol package injected), unrelated to any other security product; it is completely transparent to users — you just write POCs in standard Go and never touch the library. The rest of this document calls it "the interpreter".

The execution engine interprets your source in-process with the Go standard library and the platform's own scan symbol package pre-registered, so you can both import standard library packages and import "scan". The interpreter executes func main() automatically, and the vulnerability findings, debug logs and HTTP request count produced by this run are collected centrally.

The interpreter's symbol key format is importpath/packagename, so what the host registers is "scan/scan"; after import "scan" in a POC you can use scan.Flow / scan.Report() and so on.

The full lifecycle of one execution:

  1. Build the run environment for this execution (including the shared http.Client + Cookie session, see Chapter 6);
  2. Execute your source in a controlled goroutine;
  3. Apply timeout protection (30s by default; on timeout the run fails and returns);
  4. Return the run's vulnerability findings, debug logs, HTTP request count and error.
注意

The interpreter has no forced cancellation mechanism. After a timeout the current call returns an error immediately, but the interpreting goroutine stays in the background until it finishes on its own. Therefore never write unbounded loops or long Sleep calls in a POC, or background goroutines will accumulate.

4.4 Runtime Parameters

Runtime parameters fixed by the platform when a POC executes (you do not configure them; just know the behavior):

ParameterDefaultNotes
Single-run timeout30sAlso used as the HTTP client timeout; a timeout marks the execution as failed
Proxyempty (direct)HTTP proxy address, delivered at task level
Certificate verificationcertificate errors are ignored for both debugging and tasksMakes it easy to test self-signed lab targets
Redirectsnot followed by defaultEnabled by the debug/task side when needed
Response body limit1MBMaximum bytes read from a single response; anything beyond is truncated

Which keys actually appear in scan.Config depends on the call path:

  • Scan task: timeout, proxy, seed, taskIde;
  • GUI debugging (RunGoPocTest): timeout, proxy;
  • Unified test verification (the go branch of RunPocTest): target.

5. The Injected scan Symbols, One by One

说明

Every symbol below is accessed through the scan. prefix after import "scan". The signature in a section title is the authoritative signature you can use. String functions operate on bytes (for multi-byte UTF-8, Len/Substr count bytes), so take care with non-ASCII content.

Each section has a fixed reading order: Purpose (when to call it) → Signature (given verbatim) → Parameter table (what to pass / where it comes from / example value) → Returns (type + struct field table + what is returned when no value is available) → Code snippet (paste straight into your POC) → Notes.

Data Objects

5.1 scan.Flow

Purpose: the request flow currently being processed (passive traffic or a test packet). Almost every POC takes the target to hit from it; do not hard-code absolute URLs.

Type: struct pointer (in scripts, access it as scan.Flow.field).

Field table:

FieldTypeMeaningWho fills it, in what format
URLstringFull request URLCopied from the original URL in the packet
MethodstringRequest methodCopied from the packet (such as GET/POST)
HoststringTarget host namePrefers the domain in the packet; when empty, the host name (without port) is parsed from the URL
SchemestringSchemeStarts from the packet's TLS flag, then is overridden by the scheme parsed from the URL; the result is http/https
PathstringRequest pathFilled from the path part of the URL
QuerystringQuery parametersFilled from the URL's raw query string (without ?)
Headersmap[string]stringRequest headersParsed from the raw request header text, with keys already lowercased; access as scan.Flow.Headers["user-agent"]
BodystringRequest bodyCopied from the packet's request body and converted to a string
RawHeadersstringRaw request header text (multiple lines)Copied from the packet's raw request header text

Code snippet:

scan.Log(scan.Flow.Method + " " + scan.Flow.URL)
scan.Log("host=" + scan.Flow.Host + " path=" + scan.Flow.Path + " q=" + scan.Flow.Query)

// 取请求头(key 是小写)
if ua, ok := scan.Flow.Headers["user-agent"]; ok {
	scan.Log("UA=" + ua)
}

// 用 Flow 里的 Body 直接判定
if scan.Contains(scan.Flow.Body, "password") {
	scan.Report(scan.VulnFinding{
		Name:   "请求体敏感字段",
		Detail: "被动流量请求体中出现 password 字段",
		Level:  "low",
		URL:    scan.Flow.URL,
	})
}

Notes:

  1. When no flow is available you receive an empty Flow (Headers is an empty map and the remaining fields are empty strings); it does not panic.
  2. Headers keys are always lowercase; scan.Flow.Headers["User-Agent"] retrieves nothing.
  3. When you need the "complete current request as raw text", read RawHeaders + Body; do not assemble it yourself.

5.2 scan.Config

Purpose: read task-level configuration (timeout/proxy/random seed, etc.). Different call paths inject different keys, so always check for an empty value before using one.

Type: map[string]string.

Value table:

KeyWhen it appearsMeaningExample value
timeoutScan task, GUI debuggingExecution timeout (seconds)"30"
proxyScan task, GUI debuggingProxy address"http://127.0.0.1:8080"
seedScan taskScan speed/depth seed"1"
taskIdeScan taskTask identifier"task-..."
targetUnified test verification (RunPocTest)Target URL"https://target/"

Code snippet:

timeout := scan.Config["timeout"]
if timeout == "" {
	timeout = "30"
}
proxy := scan.Config["proxy"]
if proxy != "" {
	scan.Log("本次经代理: " + proxy)
}
scan.Log("timeout=" + timeout + " target=" + scan.Config["target"])

Notes:

  1. scan.Config["missing"] returns an empty string rather than an error; even so, your code must tolerate empty values.
  2. The same key may be absent on some entry points (for example RunPocTest has no seed); never assume it is present.
  3. Config is for reading only; do not write into it (writes would not affect the host anyway).

Types

5.3 scan.VulnFinding

Purpose: the argument to scan.Report; a complete description of one vulnerability finding. You construct it once you decide something "hit".

How to construct: scan.VulnFinding{field: value, ...} (the type symbol itself is injected by the host, and field names must be exact).

Field table:

FieldTypeMeaningHow to use
NamestringVulnerability nameEmpty uses the template's @meta:Name; when set it overrides the card title and clears the template's multilingual snapshot
DetailstringVulnerability detailsEmpty uses the template description; when set it overrides the card's "Details"
EvidencestringHit evidence (response fragment / message text)Displayed as the "response" when Request/Response are not set
LevelstringSeverity critical/high/medium/lowMapped to 4/3/2/1; empty or unrecognized falls back to the template level, and if that is also empty, low severity
URLstringHit URLEmpty uses the current Flow.URL
CVEIdstringCVE numberEmpty uses the template's @meta:CVEId; when set it overrides
RequeststringTriggering request message (text)Displayed together with Response as a request-response pair
ResponsestringHit response message (text)Same as above
SensitiveTextstringRaw fragment of the sensitive information (200 characters before and after the hit)Used only by sensitive-information templates; highlighted in the GUI
SensitiveKeywords[]stringList of keywords actually matchedThe GUI plain-text viewer highlights based on it
SensitiveMatchRulestringFormula/rule actually matchedSensitive-information templates record the matching basis here

Code snippet:

scan.Report(scan.VulnFinding{
	Name:     "未授权访问",
	Detail:   "后台接口在未携带凭证时返回了管理数据",
	Evidence: scan.Substr(resp.Body, 0, 200),
	Level:    "high",
	URL:      resp.URL,
	CVEId:    "CVE-2024-0001",
	Request:  "GET " + scan.Flow.URL,
	Response: resp.RawHeaders + "\n\n" + scan.Substr(resp.Body, 0, 500),
})

Notes:

  1. Level accepts only the four lowercase values critical/high/medium/low; HIGH or 高危 is treated as unrecognized.
  2. SensitiveText / SensitiveKeywords / SensitiveMatchRule are specific to vuln-000007 sensitive-information highlighting; ordinary POCs need not set them.
  3. One main() may call Report several times; each call produces its own vulnerability card (subject to the 404-baseline false-positive gate in Chapter 7).

5.4 scan.HTTPResponse

Purpose: the return type of every HTTP call (scan.HTTP and the convenience wrappers). It is how you tell whether a request succeeded and read the body, headers and Cookies.

Field table:

FieldTypeMeaningHow to use
StatusCodeintHTTP status code0 means the request failed (read Error in that case); otherwise 200/302/404 and so on
BodystringResponse body textTrimmed by the 1MB limit (MaxBodySize)
Headersmap[string]stringResponse headers, lowercase keysresp.Headers["content-type"]
Cookies[]stringRaw list of response Set-Cookie valueslen(resp.Cookies) > 0 tells whether a session was issued
URLstringFinal request URL (including after redirects)resp.URL is more accurate when reporting
TimeMsint64Request duration (milliseconds)For time-based blind detection
RawHeadersstringRaw response header text (multiple lines)Concatenate with Body to display the response message
ErrorstringError message when the request fails (empty on success)Check Error before checking StatusCode

Code snippet:

resp := scan.HTTPGet("https://example.com/")
if resp.Error != "" {
	scan.Log("请求失败: " + resp.Error)
	return
}
scan.Log("状态码=" + scan.JSONDump(resp.StatusCode) + " 耗时=" + scan.JSONDump(resp.TimeMs) + "ms")
scan.Log("content-type=" + resp.Headers["content-type"])
scan.Log("Set-Cookie 数=" + scan.JSONDump(len(resp.Cookies)))

Notes:

  1. On a failed request StatusCode is 0, Body is empty and Error is non-empty; always check Error first.
  2. Headers keeps only the first value of a repeated header, and all keys are lowercase.
  3. Body reads at most MaxBodySize (1MB by default); anything beyond is silently truncated.
  4. The scan package does not inject Itoa; to splice numbers into a log, use scan.JSONDump(number) (which returns "123").

Core

5.5 scan.Log(msg string)

Purpose: emit a debug log. This is the only way to trace "how far the POC got"; both the GUI debug panel and the node logs show it.

Signature:

scan.Log(msg string)

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
msgstringAny debug textConstants, scan.Flow.URL, a fragment of resp.Body, etc."开始检测"

Returns: nothing. The log is appended to this run's log buffer and handed back to the node with the execution result.

Code snippet:

scan.Log("=== 阶段 1/3:探测 ===")
resp := scan.HTTPGet(scan.Flow.URL)
scan.Log("状态码=" + scan.JSONDump(resp.StatusCode) + " len=" + scan.JSONDump(scan.Len(resp.Body)))
scan.Log("片段=" + scan.Substr(resp.Body, 0, 120))

Notes:

  1. Do not print a huge response body (such as the whole resp.Body) to the log; it blows up the debug panel and the TCP payload. Truncate with scan.Substr.
  2. scan.Log never interrupts execution; it is pure output.
  3. Log order is code order, so it can be used to verify which branch ran.

5.6 scan.Report(v VulnFinding)

Purpose: report a vulnerability finding. This is the POC's "hit exit" — do not call it and nothing will produce a vulnerability card, however correct the rest is.

Signature:

scan.Report(v VulnFinding)

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
vscan.VulnFindingOne findingBuild it with a struct literal (fields in 5.3)scan.VulnFinding{Name:"...", Level:"high"}

Returns: nothing. Each finding is appended to this run's result set.

Code snippet:

if scan.Contains(resp.Body, "SQL syntax") {
	scan.Report(scan.VulnFinding{
		Name:     "SQL 注入",
		Detail:   "响应出现数据库报错特征",
		Evidence: scan.Substr(resp.Body, 0, 300),
		Level:    "high",
		URL:      resp.URL,
	})
	scan.Log("已上报 SQL 注入")
} else {
	scan.Log("未命中")
}

Notes:

  1. Level is required and only the four lowercase values are recognized; an empty value falls back to the template level.
  2. If the found response page is similar to the task's 404 baseline beyond the threshold, the false-positive gate discards it outright (nothing is stored).
  3. Once Name / Detail are set, the card uses your single-language text and the template's multilingual snapshot is no longer used (see Chapter 7).

5.7 scan.HTTP(method, reqURL string, headers map[string]string, body string) scan.HTTPResponse

Purpose: send an HTTP request with an arbitrary method through the shared Cookie session. Use it when you need a custom method, headers or body.

Signature (verbatim from the authoritative symbol table injected by the platform):

scan.HTTP(method, reqURL string, headers map[string]string, body string) scan.HTTPResponse

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
methodstringHTTP method; an empty string is treated as GETA constant or scan.Flow.Method"POST"
reqURLstringFull URLscan.Flow.URL, or assembled from scan.URLHost/URLScheme"https://t/api/login"
headersmap[string]stringCustom request headers; nil adds noneA map literalmap[string]string{"Content-Type":"application/json"}
bodystringRequest body; an empty string means no bodyA constant or an assembled string"id=1' AND 1=1--"

Returns: scan.HTTPResponse (fields in 5.4). On failure Error is non-empty, StatusCode=0 and Body is an empty string.

Code snippet (custom headers + Cookie session: log in first, then access a protected endpoint):

base := scan.URLScheme(scan.Flow.URL) + "://" + scan.URLHost(scan.Flow.URL)

// 第一步:登录(自定义请求头;Set-Cookie 自动进共享 jar)
login := scan.HTTP("POST", base+"/api/login", map[string]string{
	"Content-Type": "application/json",
	"User-Agent":   "Mozilla/5.0 (TestSecScan)",
}, `{"username":"admin","password":"admin123"}`)
if login.Error != "" {
	scan.Log("登录请求失败: " + login.Error)
	return
}

// 第二步:带自定义头访问受保护接口(Cookie 由共享 jar 自动携带,无需手写)
resp := scan.HTTP("GET", base+"/api/admin/users", map[string]string{
	"X-Requested-With": "XMLHttpRequest",
	"Accept":           "application/json",
}, "")
if resp.Error == "" && resp.StatusCode == 200 && scan.Contains(resp.Body, "\"role\":\"admin\"") {
	scan.Report(scan.VulnFinding{
		Name:     "默认口令登录并访问后台",
		Detail:   "使用默认口令登录后,在同一会话下访问到受保护接口",
		Evidence: scan.Substr(resp.Body, 0, 300),
		Level:    "critical",
		URL:      resp.URL,
		Request:  "GET " + base + "/api/admin/users",
		Response: resp.RawHeaders + "\n\n" + scan.Substr(resp.Body, 0, 500),
	})
}

Notes:

  1. When method == "" the underlying layer treats it as GET; writing the method explicitly is clearer.
  2. A header with the same name overrides the Cookie from the shared jar; to "log in first, then access", do not hand-write a Cookie header with the same name.
  3. Passing nil for headers is fine; iteration will not panic.

Convenience HTTP

5.8 scan.HTTPGet(rawurl string) scan.HTTPResponse

Purpose: send a GET request. The most common probing entry point, equivalent to scan.HTTP("GET", rawurl, nil, "").

Signature:

scan.HTTPGet(rawurl string) scan.HTTPResponse

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
rawurlstringFull URLscan.Flow.URL, or an assembled target"https://target/"

Returns: scan.HTTPResponse (fields in 5.4).

Code snippet:

resp := scan.HTTPGet(scan.Flow.URL)
if resp.Error != "" || resp.StatusCode != 200 {
	scan.Log("GET 失败或无内容")
	return
}
if scan.Contains(resp.Body, "admin dashboard") {
	scan.Report(scan.VulnFinding{
		Name:     "未授权访问",
		Detail:   "未携带凭证即可访问管理页",
		Evidence: scan.Substr(resp.Body, 0, 300),
		Level:    "high",
		URL:      resp.URL,
	})
}

Notes:

  1. It adds no request headers of its own (headers is nil); use scan.HTTP / scan.HTTPHeader when a specific header is needed.
  2. It carries the shared Cookie session automatically.
  3. It does not follow redirects (unless a task/debug session sets AllowRedirects to true).

5.9 scan.HTTPPost(rawurl, body string) scan.HTTPResponse

Purpose: send a POST request. Equivalent to scan.HTTP("POST", rawurl, nil, body).

Signature:

scan.HTTPPost(rawurl, body string) scan.HTTPResponse

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
rawurlstringFull URLAn assembled target"https://target/api"
bodystringRequest bodyA form string or arbitrary text"user=admin&pass=admin"

Returns: scan.HTTPResponse (fields in 5.4).

Code snippet:

resp := scan.HTTPPost(scan.Flow.URL, "id=1' AND 1=1--")
if resp.Error == "" && scan.Contains(resp.Body, "SQL syntax") {
	scan.Report(scan.VulnFinding{
		Name:     "SQL 注入",
		Detail:   "POST 参数注入后返回数据库报错",
		Evidence: scan.Substr(resp.Body, 0, 300),
		Level:    "high",
		URL:      resp.URL,
	})
}

Notes:

  1. It does not set Content-Type automatically; forms usually need it (switch to scan.HTTP and add application/x-www-form-urlencoded explicitly).
  2. An empty body string means no request body.
  3. The shared Cookie session works as usual.

5.10 scan.HTTPJSONPost(rawurl, jsonBody string) scan.HTTPResponse

Purpose: send a JSON POST, automatically adding Content-Type: application/json. The most common choice for login and API probing.

Signature:

scan.HTTPJSONPost(rawurl, jsonBody string) scan.HTTPResponse

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
rawurlstringFull URLAn assembled target"https://target/api/login"
jsonBodystringJSON textHand-written, or scan.JSONDump(object){"user":"admin"}

Returns: scan.HTTPResponse (fields in 5.4).

Code snippet:

resp := scan.HTTPJSONPost("https://api.example.com/login", `{"user":"admin","pass":"admin"}`)
if resp.Error != "" {
	scan.Log("登录失败: " + resp.Error)
	return
}
code := scan.JSONGet(resp.Body, "code")
scan.Log("登录 code=" + code)
if code == "0" || len(resp.Cookies) > 0 {
	scan.Log("拿到会话,继续")
}

Notes:

  1. It only adds Content-Type; it does not serialize a map into JSON, so jsonBody must be a valid JSON string.
  2. If the target needs other headers (such as X-Token), switch to scan.HTTP / scan.HTTPHeader.
  3. Response Set-Cookie values automatically enter the shared jar.

5.11 scan.HTTPHeader(method, reqURL string, headers map[string]string, body string) scan.HTTPResponse

Purpose: an alias of scan.HTTP (the host injects the same function); the semantic name emphasizes "custom headers are attached".

Signature:

scan.HTTPHeader(method, reqURL string, headers map[string]string, body string) scan.HTTPResponse

Parameter table: identical to scan.HTTP.

ParameterTypeWhat to passWhere it comes fromExample value
methodstringHTTP method; empty means GETA constant"PUT"
reqURLstringFull URLscan.Flow.URL"https://t/api"
headersmap[string]stringCustom request headersA map literalmap[string]string{"Authorization":"Bearer x"}
bodystringRequest body; an empty string means noneA constant""

Returns: scan.HTTPResponse (fields in 5.4).

Code snippet:

resp := scan.HTTPHeader("GET", scan.Flow.URL, map[string]string{
	"Authorization": "Bearer " + scan.Config["token"],
	"Accept":        "application/json",
}, "")
if resp.Error == "" && resp.StatusCode == 200 {
	scan.Log("鉴权接口可访问,len=" + scan.JSONDump(scan.Len(resp.Body)))
}

Notes:

  1. It is the same function as scan.HTTP with identical behavior; pick whichever reads better.
  2. Do not use scan.HTTP and scan.HTTPHeader side by side in a confusing way; stay consistent within a file.
  3. Request header key case is normalized by the underlying Header.Set, so do not worry about it.

Strings

5.12 scan.ToLower(s string) string

Purpose: convert to lowercase. The standard step before case-insensitive matching.

Signature: scan.ToLower(s string) string (equivalent to strings.ToLower).

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
sstringAny stringresp.Body, etc."Root:X:0:0"

Returns: string. An empty string in, an empty string out.

Code snippet:

if scan.Contains(scan.ToLower(resp.Body), "root:x:0:0") {
	scan.Log("命中 passwd 特征")
}

Notes: it performs a Unicode lowercase mapping only and does not change length semantics.

5.13 scan.ToUpper(s string) string

Purpose: convert to uppercase.

Signature: scan.ToUpper(s string) string (equivalent to strings.ToUpper).

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
sstringAny stringresp.Body"abc"

Returns: string.

Code snippet:

scan.Log("UPPER=" + scan.ToUpper(scan.Substr(resp.Body, 0, 20)))

Notes: combine with ToLower for normalized comparisons.

5.14 scan.Trim(s string) string

Purpose: strip leading and trailing whitespace (including newlines and tabs). Commonly used to clean response fragments or form values.

Signature: scan.Trim(s string) string (equivalent to strings.TrimSpace).

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
sstringAny stringResponse text" admin\n"

Returns: string (the result after leading and trailing whitespace is removed).

Code snippet:

token := scan.Trim(scan.RegexExtract(resp.Body, `token:\s*(\S+)`))
if token != "" {
	scan.Log("token=" + token)
}

Notes: it strips only the ends, not whitespace in the middle.

5.15 scan.TrimCut(s, cutset string) string

Purpose: strip any characters in the specified set of characters from both ends (note: not a whole substring).

Signature: scan.TrimCut(s, cutset string) string (equivalent to strings.Trim).

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
sstringString to trimResponse text"///admin///"
cutsetstringSet of characters to removeA constant"/"

Returns: string.

Code snippet:

p := scan.TrimCut("/admin/users/", "/")
scan.Log("clean path=" + p) // 输出 admin/users

Notes:

  1. The second parameter is a "character set", not a "substring": TrimCut(s, "ab") removes every leading and trailing a or b.
  2. To trim a whole substring, use scan.Replace(s, substring, "").

5.16 scan.Replace(s, old, new string) string

Purpose: replace all occurrences of old in s with new.

Signature: scan.Replace(s, old, new string) string (equivalent to strings.ReplaceAll).

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
sstringOriginal stringResponse text"a-b-c"
oldstringText to be replacedA constant"-"
newstringReplacement textA constant"_"

Returns: string.

Code snippet:

clean := scan.Replace(scan.Flow.Path, "..", "")
scan.Log("clean=" + clean)

Notes: when old is an empty string it inserts new between every character (same as strings.ReplaceAll), which is usually not what you want.

5.17 scan.Split(s, sep string) []string

Purpose: split a string into a slice by a separator.

Signature: scan.Split(s, sep string) []string (equivalent to strings.Split).

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
sstringOriginal stringResponse text"a,b,c"
sepstringSeparatorA constant","

Returns: []string; when sep is empty the string is split by character.

Code snippet:

parts := scan.Split(resp.Headers["set-cookie"], ";")
for i := 0; i < len(parts); i++ {
	scan.Log("cookie part[" + scan.JSONDump(i) + "]=" + scan.Trim(parts[i]))
}

Notes:

  1. Under interpretation, an index + len() loop is the most reliable; avoid type-inference differences introduced by range.
  2. Use scan.Join to put the slice back together.

5.18 scan.Join(elems []string, sep string) string

Purpose: join a string slice into one string with a separator.

Signature: scan.Join(elems []string, sep string) string (equivalent to strings.Join).

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
elems[]stringString sliceThe result of scan.Split, scan.RegexExtractAll, etc.[]string{"a","b"}
sepstringSeparatorA constant","

Returns: string.

Code snippet:

lines := scan.Split(scan.Flow.RawHeaders, "\n")
scan.Log("头行数=" + scan.JSONDump(len(lines)) + " 拼接=" + scan.Join(lines, " | "))

Notes: a nil or empty elems returns an empty string.

5.19 scan.Contains(s, substr string) bool

Purpose: test whether s contains a substring. The workhorse for hit decisions.

Signature: scan.Contains(s, substr string) bool (equivalent to strings.Contains).

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
sstringString to searchresp.Body"hello world"
substrstringSubstring to findA constant"world"

Returns: bool (true when contained). An empty substr is always true.

Code snippet:

if scan.Contains(resp.Body, "SQL syntax") || scan.Contains(resp.Body, "mysql_fetch") {
	scan.Report(scan.VulnFinding{Name: "SQL 报错", Level: "high", Evidence: scan.Substr(resp.Body, 0, 200)})
}

Notes: it is case-sensitive; run scan.ToLower first for case-insensitive matching.

5.20 scan.HasPrefix(s, prefix string) bool

Purpose: test a prefix.

Signature: scan.HasPrefix(s, prefix string) bool (equivalent to strings.HasPrefix).

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
sstringString to checkresp.Body"http://x"
prefixstringPrefixA constant"http"

Returns: bool.

Code snippet:

if scan.HasPrefix(resp.Body, "<?xml") {
	scan.Log("响应是 XML")
}

Notes: an empty prefix is always true.

5.21 scan.HasSuffix(s, suffix string) bool

Purpose: test a suffix.

Signature: scan.HasSuffix(s, suffix string) bool (equivalent to strings.HasSuffix).

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
sstringString to checkA path"/admin/"
suffixstringSuffixA constant"/"

Returns: bool.

Code snippet:

if scan.HasSuffix(scan.Flow.Path, ".php") {
	scan.Log("PHP 目标")
}

Notes: an empty suffix is always true.

5.22 scan.Index(s, substr string) int

Purpose: return the byte index of the first occurrence of the substring, or -1 when it is absent.

Signature: scan.Index(s, substr string) int (equivalent to strings.Index).

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
sstringString to searchresp.Body"abcabc"
substrstringSubstringA constant"bc"

Returns: int; -1 when not found (never panics).

Code snippet:

pos := scan.Index(resp.Body, "password")
if pos >= 0 {
	scan.Log("password 出现在字节位置 " + scan.JSONDump(pos))
}

Notes: the value is a byte index and is not the character position for non-ASCII text.

5.23 scan.Substr(s string, start, end int) string

Purpose: take the substring s[start:end], clamping out-of-range indexes automatically. The standard tool for truncating logs and evidence.

Signature: scan.Substr(s string, start, end int) string.

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
sstringOriginal stringresp.Body"abcdef"
startintStart index (inclusive)A constant0
endintEnd index (exclusive)A constant3

Returns: string. Clamping rules: start<0 → 0; end>len(s) → len(s); start>=end → an empty string.

Code snippet:

scan.Log("前 200 字节=" + scan.Substr(resp.Body, 0, 200))
scan.Log("倒数片段=" + scan.Substr(resp.Body, scan.Len(resp.Body)-100, scan.Len(resp.Body)))

Notes:

  1. It slices by byte, so it may cut a multi-byte UTF-8 character in half; be lenient with non-ASCII content.
  2. start >= end, or any out-of-range empty range, returns an empty string and never panics.

5.24 scan.Len(s string) int

Purpose: get the byte length of a string.

Signature: scan.Len(s string) int.

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
sstringAny stringresp.Body"hello"

Returns: int (a byte count, not a character count).

Code snippet:

if scan.Len(resp.Body) > 5000 {
	scan.Log("响应较大,仅取前段判定")
}

Notes: one Chinese character is usually 3 bytes, so Len("中文")==6.

5.25 scan.Repeat(s string, count int) string

Purpose: repeat a string count times.

Signature: scan.Repeat(s string, count int) string (equivalent to strings.Repeat).

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
sstringOriginal stringA constant"A"
countintRepetitionsA constant5

Returns: string.

Code snippet:

// 构造超长参数触发异常
payload := scan.Repeat("A", 5000)
resp := scan.HTTPPost(scan.Flow.URL, "name="+payload)
scan.Log("状态码=" + scan.JSONDump(resp.StatusCode))

Notes:

  1. A negative count panics (same as strings.Repeat), so make sure it is non-negative.
  2. Do not build excessively large strings; they consume execution time and memory.

Encoding / Hashing

5.26 scan.Base64Encode(s string) string

Purpose: standard Base64 encoding (commonly used to build Basic auth or encode payloads).

Signature: scan.Base64Encode(s string) string (base64.StdEncoding.EncodeToString).

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
sstringPlain textCredentials, a payload"admin:admin"

Returns: string (Base64 text).

Code snippet:

auth := scan.Base64Encode("admin:admin")
resp := scan.HTTPHeader("GET", scan.Flow.URL, map[string]string{
	"Authorization": "Basic " + auth,
}, "")
scan.Log("Basic 状态码=" + scan.JSONDump(resp.StatusCode))

Notes: it uses the standard alphabet (including + / =), not the URL-safe variant.

5.27 scan.Base64Decode(s string) string

Purpose: Base64 decoding; returns an empty string when decoding fails.

Signature: scan.Base64Decode(s string) string.

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
sstringBase64 textAn encoded field in the response"YWRtaW4="

Returns: string; invalid Base64 returns an empty string (no error).

Code snippet:

decoded := scan.Base64Decode(scan.RegexExtract(resp.Body, `data=([A-Za-z0-9+/=]+)`))
if scan.Contains(decoded, "secret") {
	scan.Report(scan.VulnFinding{Name: "编码信息泄露", Level: "medium", Evidence: scan.Substr(decoded, 0, 200)})
}

Notes: only the standard alphabet is accepted; URL-safe input (-_) fails and returns an empty string.

5.28 scan.URLEncode(s string) string

Purpose: URL query escaping (encodes & / space and so on).

Signature: scan.URLEncode(s string) string (equivalent to url.QueryEscape).

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
sstringPlain textA parameter value"a b&c"

Returns: string.

Code snippet:

payload := "1' OR '1'='1"
resp := scan.HTTPGet(scan.Flow.URL + "/?id=" + scan.URLEncode(payload))
scan.Log("状态码=" + scan.JSONDump(resp.StatusCode))

Notes: QueryEscape encodes a space as +; encode path segments another way.

5.29 scan.URLDecode(s string) string

Purpose: URL unescaping; returns an empty string on failure.

Signature: scan.URLDecode(s string) string.

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
sstringEscaped stringA response or URL"a%20b"

Returns: string; an invalid escape returns an empty string.

Code snippet:

raw := scan.URLDecode(scan.Flow.Query)
scan.Log("解码后的查询参数=" + raw)

Notes: it correctly handles + as a space (QueryUnescape semantics).

5.30 scan.HexEncode(s string) string

Purpose: hexadecimal encoding.

Signature: scan.HexEncode(s string) string.

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
sstringPlain textAnything"abc"

Returns: string (lowercase hexadecimal).

Code snippet:

scan.Log("hex=" + scan.HexEncode("abc")) // 616263

Notes: the output is lowercase.

5.31 scan.HexDecode(s string) string

Purpose: hexadecimal decoding; returns an empty string on failure.

Signature: scan.HexDecode(s string) string.

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
sstringHexadecimal textA response field"616263"

Returns: string; an odd length or an invalid character returns an empty string.

Code snippet:

b := scan.HexDecode("616263")
scan.Log("decoded=" + b) // abc

Notes: only even-length hexadecimal strings are accepted.

5.32 scan.MD5(s string) string

Purpose: compute a lowercase hexadecimal MD5 digest (a weak hash for signature/fingerprint comparison, not for security).

Signature: scan.MD5(s string) string.

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
sstringPlain textAnything"admin"

Returns: string (32 lowercase hexadecimal characters).

Code snippet:

sig := scan.MD5("token=" + scan.RandString(8))
scan.Log("md5=" + sig)

Notes: MD5 is only for verification/fingerprints and must not be used for cryptographic security.

5.33 scan.SHA1(s string) string

Purpose: lowercase hexadecimal SHA1 digest.

Signature: scan.SHA1(s string) string.

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
sstringPlain textAnything"abc"

Returns: string (40 lowercase hexadecimal characters).

Code snippet:

scan.Log("sha1=" + scan.SHA1("abc"))

Notes: the output format matches MD5/SHA256; all are lowercase hexadecimal.

5.34 scan.SHA256(s string) string

Purpose: lowercase hexadecimal SHA256 digest.

Signature: scan.SHA256(s string) string.

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
sstringPlain textAnything"abc"

Returns: string (64 lowercase hexadecimal characters).

Code snippet:

if scan.SHA256(resp.Body) == scan.Config["expectHash"] {
	scan.Log("内容哈希匹配")
}

Notes: it hashes the raw bytes with no normalization.

Regex

5.35 scan.RegexMatch(s, pattern string) bool

Purpose: test whether a regular expression matches (without extracting anything).

Signature: scan.RegexMatch(s, pattern string) bool.

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
sstringText to searchresp.Body"root:x:0:0"
patternstringRegular expressionA constant"(?i)root:x:0:0"

Returns: bool; when the regex is invalid, the error from regexp.MatchString is ignored and false is returned.

Code snippet:

if scan.RegexMatch(resp.Body, `(?i)root:x:0:0`) {
	scan.Report(scan.VulnFinding{Name: "敏感文件泄露", Level: "high", Evidence: scan.Substr(resp.Body, 0, 200)})
}

Notes: a wrong regex raises no error, it just never matches — while debugging, print the text to be matched with scan.Log first.

5.36 scan.RegexExtract(s, pattern string) string

Purpose: extract the first match; when pattern contains capture groups it returns group 1, otherwise the whole match.

Signature: scan.RegexExtract(s, pattern string) string.

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
sstringText to searchresp.Body"user: admin"
patternstringRegex (capture groups recommended)A constant"user:\s*(\w+)"

Returns: string; an empty string when there is no match, the regex is invalid, or group 1 is empty with no whole match.

Code snippet:

user := scan.RegexExtract(resp.Body, `root:([^:]+)`)
if user != "" {
	scan.Log("提取到 user=" + user)
}

Notes:

  1. When there are several capture groups, only group 1 is returned (m[1]).
  2. When group 1 matches an empty string it falls back to the whole match m[0].
  3. To obtain several groups, write several RegexExtract calls.

5.37 scan.RegexExtractAll(s, pattern, sep string) string

Purpose: extract all matches and join them into one string with sep.

Signature: scan.RegexExtractAll(s, pattern, sep string) string.

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
sstringText to searchresp.Body"a1 b2 c3"
patternstringRegexA constant"\d+"
sepstringJoin separatorA constant","

Returns: string; an invalid regex returns an empty string; no match also returns an empty string.

Code snippet:

nums := scan.RegexExtractAll(resp.Body, `\d+`, ",")
scan.Log("所有数字=" + nums)

Notes: the return value is a joined string, not a slice; to get a slice, use scan.Split(nums, ",").

JSON

5.38 scan.JSONGet(jsonStr, path string) string

Purpose: read a value from a JSON string by dot path. The path may mix object keys and array indexes.

Signature: scan.JSONGet(jsonStr, path string) string.

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
jsonStrstringJSON textresp.Body{"data":{"role":"admin"}}
pathstringDot path supporting a.b.c and items.0.nameA constant"data.role"

Returns: string. Rules: string values are returned as-is; null → an empty string; objects/arrays → serialized back into a JSON string; a missing path or invalid JSON → an empty string.

Code snippet:

if !scan.JSONValid(resp.Body) {
	scan.Log("响应不是 JSON,跳过")
	return
}
role := scan.JSONGet(resp.Body, "data.user.role")
first := scan.JSONGet(resp.Body, "items.0.name")
scan.Log("role=" + role + " first=" + first)
if role == "admin" {
	scan.Report(scan.VulnFinding{Name: "越权", Level: "critical", Evidence: scan.Substr(resp.Body, 0, 300)})
}

Notes:

  1. Numeric values are also returned as strings (such as "1"); remember the quotes when comparing.
  2. Empty path segments are handled with Trim(path, "."), which removes leading and trailing dots.
  3. To take a nested object as a whole, JSONGet returns its JSON text.

5.39 scan.JSONValid(s string) bool

Purpose: test whether a string is valid JSON.

Signature: scan.JSONValid(s string) bool (json.Valid).

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
sstringText to checkresp.Body{"ok":true}

Returns: bool.

Code snippet:

if scan.JSONValid(resp.Body) {
	scan.Log("code=" + scan.JSONGet(resp.Body, "code"))
} else {
	scan.Log("非 JSON 响应,走正则回退分支")
}

Notes: it validates syntax only and does not parse the structure; a later value lookup can still return an empty string because the path does not exist.

5.40 scan.JSONDump(v interface{}) string

Purpose: serialize any value into a JSON string. It is also the standard way to splice numbers/booleans into a log (scan does not inject Itoa).

Signature: scan.JSONDump(v interface{}) string.

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
vinterface{}Any valueA map/slice/number/booleanmap[string]int{"code":200}

Returns: string; serialization failure returns an empty string.

Code snippet:

scan.Log("状态码=" + scan.JSONDump(resp.StatusCode) + " 耗时=" + scan.JSONDump(resp.TimeMs))
scan.Log("汇总=" + scan.JSONDump(map[string]int{"http": 1, "found": 1}))

Notes:

  1. A number yields "200" while a string yields a quoted "\"ok\"" — when building logs, prefer numbers/maps over bare strings.
  2. Values that cannot be serialized (such as functions) return an empty string.

Random / Time

5.41 scan.RandInt(min, max int) int

Purpose: generate a random integer in the range [min, max).

Signature: scan.RandInt(min, max int) int.

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
minintLower bound (inclusive)A constant1
maxintUpper bound (exclusive)A constant100

Returns: int; when max<=min it is treated as max=min+1, that is, it returns min.

Code snippet:

n := scan.RandInt(1, 100)
scan.Log("随机整数=" + scan.JSONDump(n))
if n < 50 {
	scan.Log("落在上半区")
}

Notes: it uses crypto/rand and is concurrency-safe; the range is half-open.

5.42 scan.RandString(n int) string

Purpose: generate an n-character random string (upper and lower case letters + digits). Ideal as a reverse-connection marker or a cache-busting parameter.

Signature: scan.RandString(n int) string.

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
nintDesired lengthA constant12

Returns: string; when n<=0 it uses 8 characters.

Code snippet:

marker := scan.RandString(12)
resp := scan.HTTPGet(scan.Flow.URL + "/?cache=" + marker)
scan.Log("marker=" + marker + " 状态码=" + scan.JSONDump(resp.StatusCode))

Notes: the alphabet is a-zA-Z0-9; for letters only or digits only use RandAlpha/RandNum.

5.43 scan.RandAlpha(n int) string

Purpose: generate an n-character random lowercase letter string.

Signature: scan.RandAlpha(n int) string.

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
nintDesired lengthA constant6

Returns: string; when n<=0 it uses 8 characters.

Code snippet:

scan.Log("alpha=" + scan.RandAlpha(6))

Notes: the alphabet is only abcdefghijklmnopqrstuvwxyz, with no uppercase.

5.44 scan.RandNum(n int) string

Purpose: generate an n-character random digit string.

Signature: scan.RandNum(n int) string.

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
nintDesired lengthA constant6

Returns: string; when n<=0 it uses 8 characters.

Code snippet:

scan.Log("num=" + scan.RandNum(6))

Notes: the alphabet is only 0123456789; the first character may be 0.

5.45 scan.UUID() string

Purpose: generate a UUID v4. Useful as an idempotency key or a temporary resource name.

Signature: scan.UUID() string (no parameters).

Parameter table: none.

Returns: string, shaped like xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx.

Code snippet:

id := scan.UUID()
resp := scan.HTTPJSONPost(scan.Flow.URL, `{"id":"`+id+`"}`)
scan.Log("uuid=" + id + " 状态码=" + scan.JSONDump(resp.StatusCode))

Notes: every call generates a new value (crypto/rand).

5.46 scan.Timestamp() int64

Purpose: get the current Unix timestamp in seconds.

Signature: scan.Timestamp() int64 (no parameters).

Parameter table: none.

Returns: int64 (Unix seconds).

Code snippet:

scan.Log("ts=" + scan.JSONDump(scan.Timestamp()))

Notes: the return value is int64; splice it into a log with scan.JSONDump.

5.47 scan.TimestampMs() int64

Purpose: get the current Unix timestamp in milliseconds.

Signature: scan.TimestampMs() int64 (no parameters).

Parameter table: none.

Returns: int64 (Unix milliseconds).

Code snippet:

start := scan.TimestampMs()
scan.HTTPGet(scan.Flow.URL)
scan.Log("耗时≈" + scan.JSONDump(scan.TimestampMs()-start) + "ms")

Notes: to measure durations prefer resp.TimeMs; a TimestampMs difference includes extra overhead.

5.48 scan.Sleep(ms int)

Purpose: sleep for the given number of milliseconds. Used for reverse-connection polling and time-based blind waits.

Signature: scan.Sleep(ms int) (no return value).

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
msintMilliseconds to sleepA constant500

Returns: nothing.

Code snippet:

scan.HTTPGet(scan.Flow.URL)
scan.Sleep(500) // 等后端异步处理
resp := scan.HTTPGet(scan.Flow.URL + "/result")
scan.Log("轮询结果 len=" + scan.JSONDump(scan.Len(resp.Body)))

Notes:

  1. Sleep consumes the execution time budget, whose total timeout is 30s by default; control both the number of polls and the duration of each.
  2. The interpreter cannot force cancellation, so a long Sleep keeps running in the background after the timeout.

URL Parsing

5.49 scan.URLHost(rawurl string) string

Purpose: get the host name of a URL (without the port).

Signature: scan.URLHost(rawurl string) string.

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
rawurlstringFull URLscan.Flow.URL"https://a.com:8443/x"

Returns: string; parsing failure returns an empty string. Note that Hostname() strips the port.

Code snippet:

host := scan.URLHost(scan.Flow.URL)
base := scan.URLScheme(scan.Flow.URL) + "://" + host
scan.Log("站点根=" + base)

Notes: URLHost returns the host name rather than the Host (no port); parse it yourself if the port is needed.

5.50 scan.URLPath(rawurl string) string

Purpose: get the path part of a URL.

Signature: scan.URLPath(rawurl string) string.

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
rawurlstringFull URLscan.Flow.URL"https://a.com/a/b?x=1"

Returns: string (such as /a/b); parsing failure returns an empty string.

Code snippet:

scan.Log("path=" + scan.URLPath(scan.Flow.URL))

Notes: the returned path excludes the query string (use URLQuery for that).

5.51 scan.URLQuery(rawurl string) string

Purpose: get the raw query string (without ?).

Signature: scan.URLQuery(rawurl string) string.

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
rawurlstringFull URLscan.Flow.URL"https://a.com/a?x=1&y=2"

Returns: string (such as x=1&y=2); parsing failure or no query returns an empty string.

Code snippet:

if scan.URLQuery(scan.Flow.URL) != "" {
	scan.Log("带参目标: " + scan.URLDecode(scan.URLQuery(scan.Flow.URL)))
}

Notes: it returns the undecoded raw string; pair it with scan.URLDecode for readable text.

5.52 scan.URLScheme(rawurl string) string

Purpose: get the scheme (http/https).

Signature: scan.URLScheme(rawurl string) string.

Parameter table:

ParameterTypeWhat to passWhere it comes fromExample value
rawurlstringFull URLscan.Flow.URL"https://a.com/"

Returns: string (http or https); parsing failure returns an empty string.

Code snippet:

if scan.URLScheme(scan.Flow.URL) == "https" {
	scan.Log("HTTPS 目标")
}

Notes: to assemble a site root, the usual form is scan.URLScheme + "://" + scan.URLHost.

5.53 Full Symbol Table (Authoritative List)

重要

The table below is the authoritative symbol list. Every scan.xxx in a POC must appear in it; symbols outside it (such as scan.Itoa) do not exist, and using one makes interpretation fail.

CategoryAll symbols
Datascan.Flow、scan.Config
Typesscan.VulnFinding、scan.HTTPResponse
Corescan.Log、scan.Report、scan.HTTP
Convenience HTTPscan.HTTPGet、scan.HTTPPost、scan.HTTPJSONPost、scan.HTTPHeader
Stringsscan.ToLower、scan.ToUpper、scan.Trim、scan.TrimCut、scan.Replace、scan.Split、scan.Join、scan.Contains、scan.HasPrefix、scan.HasSuffix、scan.Index、scan.Substr、scan.Len、scan.Repeat
Encoding/Hashingscan.Base64Encode、scan.Base64Decode、scan.URLEncode、scan.URLDecode、scan.HexEncode、scan.HexDecode、scan.MD5、scan.SHA1、scan.SHA256
Regexscan.RegexMatch、scan.RegexExtract、scan.RegexExtractAll
JSONscan.JSONGet、scan.JSONValid、scan.JSONDump
Random/Timescan.RandInt、scan.RandString、scan.RandAlpha、scan.RandNum、scan.UUID、scan.Timestamp、scan.TimestampMs、scan.Sleep
URL parsingscan.URLHost、scan.URLPath、scan.URLQuery、scan.URLScheme

Every POC run creates a shared http.Client + net/http/cookiejar, and all HTTP calls (scan.HTTP / scan.HTTPGet / scan.HTTPPost / scan.HTTPJSONPost / scan.HTTPHeader) go through the same client. Therefore:

  • The Set-Cookie obtained by the login in step one is stored in the jar automatically;
  • A request to the same site in step two carries the Cookie automatically;
  • The two-step chain "log in first, then access a protected endpoint" works naturally, with no manual Cookie shipping.
base := scan.URLScheme(scan.Flow.URL) + "://" + scan.URLHost(scan.Flow.URL)

// 第一步:登录,Cookie 进 jar
scan.HTTPJSONPost(base+"/login", `{"user":"admin","pass":"admin"}`)

// 第二步:访问受保护接口,自动携带会话 Cookie
resp := scan.HTTPGet(base + "/admin/profile")
if resp.Error == "" && resp.StatusCode == 200 {
	scan.Log("会话有效,len=" + scan.JSONDump(scan.Len(resp.Body)))
}

The headers parameter: the third argument of scan.HTTP(method, reqURL, headers, body) is a map[string]string used to set request headers; scan.HTTPHeader is exactly equivalent, with a more explicit name. If you set a Cookie header with the same name by hand, the explicit header overrides the same-named value in the session — that is normal behavior, but if you want to "log in first", do not hand-write a same-named Cookie.

提示

Chains of three or more steps (login → privilege escalation → data retrieval) also work through the shared jar; just keep them executing sequentially inside the main() of the same run.


7. Returning Results: scan.Report and Vulnerability Cards

After scan.Report(scan.VulnFinding{...}) is called, the node assembles each finding into a vulnerability detail and reports it to the controller for storage. How each field affects the final vulnerability card:

VulnFinding fieldEffect on the card
NameOverrides the template's @meta:Name as the card title; once overridden, the template's multilingual snapshot is cleared (the name produced by the POC is single-language text)
DetailOverrides the template description as the card's "Details"; it likewise clears the multilingual snapshot
EvidenceWhen Request/Response are not set, it is displayed as the "response" in the request-response list
LevelMapped to a vulnerability severity: critical→4, high→3, medium→2, low→1; unrecognized or empty is treated as low severity; when empty it falls back to the template's @meta:Level
URLHit URL; empty uses the current Flow.URL
CVEIdOverrides the template's CVE number
Request / ResponseDisplayed as a request-response message pair
SensitiveText / SensitiveKeywords / SensitiveMatchRuleThe three sensitive-information highlighting fields, used only by sensitive-information templates
注意

Before reporting there is one more gate: the 404-baseline false-positive gate. If the found response page is more similar to the task's 404 baseline than the threshold, it is judged a false positive and discarded outright. So do not report ordinary error pages as vulnerabilities, and do not leave Level empty — an empty severity is treated as low.


8. Local Development and Online Debugging

8.1 Creating a Template in the GUI

  1. Open the vulnerability configuration and click "Add Vulnerability"; choose template type Go (the UI tab is "Go template");
  2. Write the source in the editor (including the //go:build ignore line + @meta header);
  3. After saving, the controller writes poc/go/<VulnIde>.go and registers it in memory; when the template is delivered to a node, the node encrypts it into scan-poc/go/<VulnIde>.gopoc with AES.

When GoSource is empty, the controller generates a skeleton with a @meta header and package main from the metadata.

CommandDirectionPurpose
RunGoPocTestGUI → controller → scanner nodeDebug-run a hot-loadable Go POC; returns GoPocTestResult (findings + logs + HTTP count)
GetGoPocDetailGUI → controllerFetch the full details of a vulnerability (including GoSource) so editing uses the latest source
PocTestRequest / RunPocTestGUI → controller → scanner nodeUnified test verification (shared by yaml/go/wasm); returns PocTestResult

Fields of the GoPocTestRequest request structure used by RunGoPocTest: VulnIde, GoSource, TargetURL, TestFlow, Timeout (seconds, default 30), Proxy. Result structure GoPocTestResult: VulnIde, Success, Findings, Logs, HttpCount, Error.

The unified test verification entry point RunPocTest dispatches on PocTestRequest.PocType; when PocType="go" it runs through the same interpreter, and Success and Found are returned separately (Found = len(findings) > 0).

8.3 Where Test Packets Come From

While debugging you may omit the packet and use only TargetURL (the node builds a GET flow automatically). A more realistic approach is to paste a burp-style raw request message: the node parses "request line + headers + blank line + Body" and assembles the full URL from the Host header and the path in the request line. The GUI can also build a packet from a request-step index of a YAML template.

8.4 Silent Testing

The yaml branch of test verification and the debug entry point both use "return only, never store" semantics: hit details are read from the result and never written to the vulnerability database. In AI dispatch scenarios, WantExchanges=true additionally returns the raw request/response text of every step (truncated to 8KB by default) so the AI can judge for itself.

说明

The go branch of RunPocTest currently does not deliver the proxy address to the execution engine (it only puts target into Config), whereas RunGoPocTest and scan tasks do deliver it. If your target is reachable only through a proxy, use the run test on the GUI's "Go template" tab (RunGoPocTest), which is more reliable. Keep this difference in mind when debugging go templates through RunPocTest.


9. Debugging Handbook

This chapter is the quick reference for "where to look when it does not work". It first covers the three observation surfaces, then the five-minute minimal verification flow, and finally the symptom reference table.

9.1 Where to See Logs and Results

Observation surfaceWhat to look atData sourceWhen to use
GUI "Go template" debug panelscan.Log output + matched findings + HTTP request countGoPocTestResult's Logs / Findings / HttpCountVerifying logic line by line during development; the fastest route
Task-result vulnerability cardVulnerability name / details / severity / request-response messagesThe vulnerability detail reported by the node (queryable once stored)Verifying what a real scan task produces
Controller / node logsPOC execution failures, timeouts and per-run logsNode / controller logsTroubleshooting task-time issues and checking whether the POC was scheduled
Unified test verification resultPocTestResult's Success / Found / FindingsThe go branch of RunPocTestAI dispatch / test verification tab

9.2 Five-Minute Minimal Verification Flow

  1. Open the GUI vulnerability configuration, click "Add Vulnerability", choose template type Go, and create a template.
  2. Paste the minimal skeleton into the editor (copy the complete skeleton from section 2.5; give @meta:Name any value) and save.
  3. Switch to that template's "Go template" tab and enter a reachable address in "target URL" (for example http://your-lab/).
  4. Click "Run Test". The GUI sends a RunGoPocTest command → the controller forwards it to an online scanner node → the node builds the Flow and hands it to the interpreter.
  5. Inspect what comes back:
    • Every line your scan.Log printed appears in GoPocTestResult.Logs → the code really ran;
    • GoPocTestResult.HttpCount > 0 → requests really went out;
    • If Success=false, Error states whether it was an execution error or a timeout;
    • On a hit, Findings is non-empty and the GUI displays the vulnerability card.
  6. If step 5 shows nothing: check Error first, then whether Logs is empty. Empty Logs usually means main() never ran at all (see the reference table in 9.3).
提示

During debugging, scan.Log every key intermediate value (truncated with scan.Substr); it is far faster than staring at the editor and guessing.

9.3 Symptom → Cause → Fix

SymptomCauseFix
go build ./... reports illegal character U+00B0, or the source tree fails to compile//go:build ignore was forgotten, so the template is compiled as ordinary sourceThe first line must be //go:build ignore, followed by a blank line; platform-generated skeletons already include it
POC execution is clearly slower than an equivalent YAML templateEvery run goes through the interpreter (compilation + reflection), which is an order of magnitude slowerAlways use YAML when possible; reserve Go for complex algorithms that YAML cannot express
Execution reports "interpretation failed/undefined: xxx"A third-party package outside the whitelist was importedThe platform registers only the Go standard library and the scan package; switch to the scan package or an already-registered standard library package
Error Go POC 执行超时(30s)The logic exceeds the single-run timeout (30s by default); too many requests or a long SleepLimit the number of requests and loop iterations; shorten or remove Sleep; keep the total budget away from the timeout ceiling
The vulnerability card severity is wrong / shown as lowReport's Level is empty or misspelled (such as HIGH/高危)Always use the four lowercase values critical/high/medium/low
The node's go build ./... reports a ciphertext compile errorNode-side ciphertext was written as .go (a past incident: no build tag is visible in ciphertext)Store it as .gopoc on the node; the node migrates historical .go ciphertext automatically at startup
The English UI shows Chinese names/descriptionsOnly cn was written, with no .en addedAdd the .en suffix for language-related keys (Name/Description/Solution/AffectedProducts)
The vulnerability name is empty / all metadata is lost@meta: case or spelling is wrong, the comment is not at the start of the line, or @Meta:/meta: was usedSpell key names exactly; comments must start the line; whitespace around = is trimmed automatically
The run as a whole times out with logs stuck in pollingscan.Sleep consumes the execution time budgetLimit each sleep and the iteration count; leave enough headroom in the total polling time
The node process's memory/goroutines keep growing after a taskThe POC contains an unbounded loop, and the interpreter cannot force cancellation after a timeout, so the goroutine lingers in the backgroundEvery loop must have a clear upper bound; avoid for {}
The template "disappears" from the list and nothing happensThe package declaration is missing and the file is silently skipped at load time (no error)A package main (or a valid package declaration) is required
The request succeeds but nothing is detectedBesides Level, the usual cause is a wrong regex (RegexMatch returns false on an invalid regex and raises no error)Print the text to be matched with scan.Log; first verify the path with a simple scan.Contains
A target reachable only through a proxy fails to connect directlyThe go branch of RunPocTest does not deliver the proxy to the execution engine (known limitation)Use the GUI's "Go template" RunGoPocTest or a real scan task, both of which deliver the proxy
The response body's tail features cannot be matchedThe single-read limit is 1MB; anything beyond is truncatedInspect only the front part for detection; for full content, issue several ranged requests
The two-step session fails (step two is not logged in)A Cookie header with the same name as Set-Cookie was set by hand, overriding the session valueDo not hand-write a same-named Cookie when you need "log in first"; let the shared jar manage it
A generic template requests a hard-coded host in another taskThe code hard-codes an absolute URLTake the current flow from scan.Flow; the AI generation path rejects hard-coded targets outright
提示

Before submitting a template, self-check four things: is the first line //go:build ignore? Does @meta:Name start its line? Are package main and func main() present? Are all the scan.xxx symbols used listed in the full symbol table in 5.53? These four checks catch the vast majority of "the template does not work" problems.


10. Complete Examples

All examples omit the blank-line detail after //go:build ignore; in a real file keep the build constraint on the first line followed by a blank line.

10.1 Example 1: Response Feature Matching (Simplest)

//go:build ignore

// @meta:Name=未授权访问检测
// @meta:Name.en=Unauthorized Access Detection
// @meta:Level=high
// @meta:Description=检测后台接口是否可在未授权情况下访问
// @meta:Description.en=Detect whether the admin endpoint is accessible without authorization
// @meta:Solution=为接口增加鉴权
// @meta:Solution.en=Add authentication to the endpoint
// @meta:TestDepth=pack
// @meta:Confidence=85
// @meta:DefaultLanguage=cn

package main

import (
	"scan"
)

func main() {
	scan.Log("目标: " + scan.Flow.URL)

	resp := scan.HTTPGet(scan.Flow.URL)
	if resp.Error != "" {
		scan.Log("请求失败: " + resp.Error)
		return
	}

	// 状态码 200 且响应出现后台特征关键字
	if resp.StatusCode == 200 && scan.Contains(scan.ToLower(resp.Body), "admin dashboard") {
		scan.Report(scan.VulnFinding{
			Name:     "未授权访问检测",
			Detail:   "后台接口在未授权情况下返回了管理页面特征",
			Evidence: scan.Substr(resp.Body, 0, 300),
			Level:    "high",
			URL:      resp.URL,
			Response: resp.RawHeaders + "\n\n" + scan.Substr(resp.Body, 0, 500),
		})
	}
}
//go:build ignore

// @meta:Name=默认口令登录并访问后台
// @meta:Name.en=Default Credential Login and Admin Access
// @meta:Level=critical
// @meta:Description=使用默认口令登录后访问受保护接口,验证默认凭据风险
// @meta:Description.en=Log in with default credentials, then access a protected endpoint to confirm the risk
// @meta:Solution=强制修改默认口令并启用多因素认证
// @meta:Solution.en=Force password change and enable MFA
// @meta:CweId=CWE-798
// @meta:TestDepth=deep
// @meta:DefaultLanguage=cn

package main

import (
	"scan"
)

func main() {
	base := scan.URLScheme(scan.Flow.URL) + "://" + scan.URLHost(scan.Flow.URL)
	scan.Log("站点根: " + base)

	// 第一步:默认口令登录,Set-Cookie 自动进入共享会话
	loginResp := scan.HTTPJSONPost(base+"/api/login", `{"username":"admin","password":"admin123"}`)
	if loginResp.Error != "" {
		scan.Log("登录请求失败: " + loginResp.Error)
		return
	}
	scan.Log("登录状态码: " + scan.JSONDump(loginResp.StatusCode))

	// 登录响应里提示成功或已下发会话 Cookie 才继续
	loginOK := loginResp.StatusCode == 200 && (scan.Contains(loginResp.Body, "token") || len(loginResp.Cookies) > 0)
	if !loginOK {
		scan.Log("默认口令未通过,结束")
		return
	}

	// 第二步:携带会话 Cookie 访问受保护接口(无需手工搬运 Cookie)
	adminResp := scan.HTTPGet(base + "/api/admin/users")
	if adminResp.Error != "" {
		scan.Log("访问后台失败: " + adminResp.Error)
		return
	}

	// 命中判据:后台接口正常返回且出现管理数据特征
	if adminResp.StatusCode == 200 && (scan.Contains(adminResp.Body, "\"role\":\"admin\"") || scan.Contains(scan.ToLower(adminResp.Body), "user list")) {
		scan.Report(scan.VulnFinding{
			Name:     "默认口令登录并访问后台",
			Detail:   "使用默认口令成功登录,并在同一会话下访问到受保护的用户管理接口",
			Evidence: scan.Substr(adminResp.Body, 0, 300),
			Level:    "critical",
			URL:      adminResp.URL,
			CVEId:    "CVE-2024-0001",
			Request:  "GET " + base + "/api/admin/users",
			Response: adminResp.RawHeaders + "\n\n" + scan.Substr(adminResp.Body, 0, 500),
		})
	}
}

10.3 Example 3: JSON Parsing + Regex Extraction + Multi-Branch Reporting

//go:build ignore

// @meta:Name=接口信息泄露与弱令牌检测
// @meta:Name.en=API Information Disclosure and Weak Token Detection
// @meta:Name.zh=接口信息泄露与弱令牌检测
// @meta:Level=high
// @meta:Description=解析接口 JSON 响应,检测敏感字段与可预测令牌
// @meta:Description.en=Parse the API JSON response to detect sensitive fields and predictable tokens
// @meta:Solution=移除敏感字段并改用不可预测的强随机令牌
// @meta:Solution.en=Remove sensitive fields and use unpredictable strong random tokens
// @meta:CweId=CWE-200
// @meta:References=https://example.com/advisory
// @meta:References+=https://example.com/api-security
// @meta:Confidence=90
// @meta:TestDepth=deep
// @meta:DefaultLanguage=cn

package main

import (
	"scan"
)

func main() {
	resp := scan.HTTPGet(scan.Flow.URL)
	if resp.Error != "" {
		scan.Log("请求失败: " + resp.Error)
		return
	}
	if resp.StatusCode != 200 {
		scan.Log("状态码非 200,跳过")
		return
	}

	// 分支一:响应不是 JSON 时,退化为正则特征匹配
	if !scan.JSONValid(resp.Body) {
		if scan.RegexMatch(resp.Body, `(?i)(password|secret|api[_-]?key)\s*[:=]`) {
			key := scan.RegexExtract(resp.Body, `(?i)api[_-]?key\s*[:=]\s*["']?([A-Za-z0-9]{16,})`)
			scan.Report(scan.VulnFinding{
				Name:               "接口信息泄露与弱令牌检测",
				Detail:             "响应正文中出现疑似密钥/口令特征(非 JSON 回退分支)",
				Evidence:           scan.Substr(resp.Body, 0, 300),
				Level:              "high",
				URL:                resp.URL,
				SensitiveText:      scan.Substr(resp.Body, 0, 400),
				SensitiveKeywords:  []string{"api_key", key},
				SensitiveMatchRule: "regex:(?i)api[_-]?key",
			})
			return
		}
		scan.Log("无命中,结束")
		return
	}

	// 分支二:JSON 路径取敏感字段
	role := scan.JSONGet(resp.Body, "data.user.role")
	token := scan.JSONGet(resp.Body, "data.token")
	secret := scan.JSONGet(resp.Body, "data.config.secretKey")
	scan.Log("role=" + role + " secretLen=" + scan.JSONDump(scan.Len(secret)))

	if scan.ToLower(role) == "admin" && secret != "" {
		scan.Report(scan.VulnFinding{
			Name:     "接口信息泄露与弱令牌检测",
			Detail:   "JSON 响应泄露了管理员配置字段 secretKey",
			Evidence: scan.Substr(secret, 0, 120),
			Level:    "high",
			URL:      resp.URL,
			Response: scan.Substr(resp.Body, 0, 500),
		})
	}

	// 分支三:令牌可预测(纯数字/短长度)判定
	if token != "" {
		weak := scan.RegexMatch(token, `^\d+$`) || scan.Len(token) < 16
		if weak {
			// 多次采样验证可预测性
			predictable := true
			for i := 0; i < 3; i++ {
				next := scan.JSONGet(scan.HTTPGet(scan.Flow.URL).Body, "data.token")
				if next != token && !scan.HasPrefix(next, scan.Substr(token, 0, 6)) {
					predictable = false
					break
				}
				scan.Sleep(200)
			}
			if predictable {
				scan.Report(scan.VulnFinding{
					Name:     "接口信息泄露与弱令牌检测",
					Detail:   "接口返回的令牌可预测(短/纯数字且前缀稳定)",
					Evidence: token,
					Level:    "high",
					URL:      resp.URL,
					CVEId:    "CVE-2024-9999",
				})
			}
		}
	}

	// 分支四:随机与时间辅助信息(演示相关函数)
	scan.Log("样本 ID=" + scan.UUID() + " 位=" + scan.RandString(8) +
		" 字母=" + scan.RandAlpha(6) + " 数字=" + scan.RandNum(6) +
		" 秒=" + scan.JSONDump(scan.Timestamp()) +
		" 毫秒=" + scan.JSONDump(scan.TimestampMs()) +
		" 随机整数=" + scan.JSONDump(scan.RandInt(1, 100)))
}

11. Pitfall Checklist

PitfallSymptomAvoidance
Forgetting //go:build ignoreTemplates in the source tree are compiled by go build ./..., causing errors or being packagedThe first line must be //go:build ignore, followed by a blank line; platform-generated skeletons already include it
Expecting YAML-level performanceEvery run goes through the interpreter (compilation + reflection), which is an order of magnitude slowerAlways use YAML when possible; reserve Go for complex algorithms
Importing an unregistered packageimport of a third-party package → interpretation failureThe platform registers only the Go standard library and scan; extend through the scan package instead of third-party imports
The 30s timeoutExecution over 30s returns a timeout error; moreover the interpreter cannot truly cancel the goroutineLimit the number of requests and the amount of logic; scan.Sleep consumes the execution time budget
Using a symbol that does not exist in scanInterpretation reports undefined (for example, writing scan.Itoa by mistake)Use only symbols in the full table in 5.53; convert numbers to strings with scan.JSONDump
The Level value passed to ReportEmpty or misspelled → falls back to the template level, or low severity if the template has noneAlways use the four lowercase values critical/high/medium/low
The node-side extension must be .gopocCiphertext written as .go → the node's go build ./... reports illegal character U+00B0 (a past incident)Store it as .gopoc on the node; the node migrates historical .go ciphertext automatically at startup
Multilingual support requires cn + enOnly Chinese written → the English UI shows ChineseAdd the .en suffix for language-related keys (Name/Description/Solution/AffectedProducts)
@meta case and spacesA misspelled key or a wrong form of @meta: → empty metadata and an empty vulnerability nameSpell key names exactly; comments must start the line; whitespace around = is trimmed automatically
scan.Sleep consumes execution timeA long sleep causes an overall timeoutIn polling scenarios limit the count and each duration; keep the total budget away from the Timeout
Writing an unbounded loop in a POCAfter a timeout the goroutine lingers in the background, accumulating a leakEvery loop must have a clear upper bound; avoid for {}
Missing the package declarationSilently skipped at load time (no error; the template simply "disappears")package main is required
Setting a same-named Cookie by handThe explicit header overrides the same-named Cookie in the sessionWhen you need "log in first", do not hand-write a same-named Cookie header
Hard-coding an absolute URL in a generic templateOther tasks use it to request a hard-coded host (out of scope)Take the current flow from scan.Flow; the AI generation path rejects hard-coded targets outright
The response body looks truncatedThe single-read limit is 1MBFor large responses, detect on the front part only; for full content, switch to several ranged requests
scan.Headers key casescan.Flow.Headers["User-Agent"] retrieves nothingRequest/response header keys are all lowercase; write ["user-agent"]
The second parameter of scan.TrimCutAssumed to be a "substring to remove" when it is actually a "character set"TrimCut(s, cutset) removes any characters from cutset at both ends; use Replace for substrings
The go branch of RunPocTest carries no proxyA target reachable only through a proxy fails to connect directlyUse the GUI's "Go template" run test (RunGoPocTest) or a scan task; they inject the proxy
提示

Before submitting a template, self-check three things: is the first line //go:build ignore? Does @meta:Name start its line? Are all symbols used after import "scan" listed in the full symbol table in 5.53? These three checks catch the vast majority of "the template does not work" problems.

本文档随 SDK 源码同步更新;新增或修改公开接口后会同步到此页。