---
slug: go-poc-hotload
title: 扫描节点 · Go 热加载 POC 开发
titleEn: Side-loadable Go POC Development
summary: 用纯 Go 源码编写漏洞模板（扫描节点进程内解释执行，无需 Go 工具链）：@meta 注释头、scan 注入包全部符号与示例。
summaryEn: 'Write vulnerability templates in pure Go (interpreted in-process by the scanner, no Go toolchain needed): @meta header and every injected scan symbol.'
category: 扫描节点
order: 50
enabled: true
updatedAt: "2026-10-05"
---
# 扫描节点 · Go 热加载 POC 开发

> 本文面向**第三方开发者**：当 YAML 模板表达不了你的检测逻辑时，用一份纯 Go 源码 + `@meta` 注释头就能写出一条可被扫描任务选用的漏洞模板。扫描节点用**内置的 Go 解释器在进程内执行**它，**目标机不需要安装 Go 工具链**，把文件丢进加载目录即可生效（热加载）。

> [!TIP]
> 本文按「逐函数精讲」组织。第五章是核心：`scan` 注入包的**每一个公开符号**都有独立四级小节，小节内依次给出「作用 → 签名 → 参数表 → 返回（结构体给字段表）→ 可直接粘贴的代码片段 → 注意事项」。**所有签名都逐字来自平台注入的权威符号表，不存在任何未注入的符号。**

[[toc]]

---

## 一、它是什么 / 何时选它

### 1.1 形态定义

Go 热加载 POC 是**漏洞模板的一种形态**，不是插件。判定依据是模板的 `PocType` 字段：

- `PocType="yaml"` —— 声明式 YAML 模板，走扫描节点原生引擎；
- `PocType="go"` —— 本文档的主角，纯 Go 源码，走进程内解释执行；
- `PocType="wasm"` —— WASM 二进制模板，走 wazero 运行时（需签名）。

它的构成只有两样东西：

1. 文件顶部的 `// @meta:Key=Value` 注释头（**不是** YAML，是注释）；
2. 一段标准 Go 代码：`package main` + `func main()`，能力全部通过 `import "scan"` 访问宿主注入的符号包。

解释器会自动执行 `main()`，你**不需要**、也无法显式调用 `main.main()`。

### 1.2 存储与加密：控制器明文，节点密文

同一个 Go POC 在两端落盘形态不同，这是理解整条链路的关键：

| 位置 | 目录与文件名 | 内容 | 说明 |
|---|---|---|---|
| 控制器 | `poc/go/<VulnIde>.go` | 明文 Go 源码 | 控制器启动时扫描该目录加载；GUI 编辑、`GetGoPocDetail` 都读它 |
| 扫描节点 | `scan-poc/go/<VulnIde>.gopoc` | **AES-GCM 密文** | 由控制器下发给节点后，节点用全局密钥加密落盘到自己的运行目录 |

> [!NOTE]
> 早期存量按语言分目录写作 `scan-poc/<语言>/go/<VulnIde>.gopoc`；2026-09-27 多语言单文件化后，Go POC 与 yaml POC 一样**不再按语言分目录**，当前路径固定为 `scan-poc/go/<VulnIde>.gopoc`。

- **节点侧扩展名是 `.gopoc`，不是 `.go`。** 这是血的教训：密文字节里看不到 `//go:build ignore`，Go 工具链会把 `scan-poc/go/<VulnIde>.go` 当源码编译，`go build ./...` 直接报 `illegal character U+00B0`。
- 节点启动时会自动把历史遗留的 `.go` 密文改名为 `.gopoc`（best-effort）；带 `//go:build ignore` 的真实源码不会被误改。
- 读取时**先尝试 AES-GCM 解密，失败则按明文受理**，所以本地手工放一份明文 `.gopoc` 也能跑（便于调试）。

### 1.3 与 YAML / WASM 的选型对照

> [!IMPORTANT]
> **能用 YAML 实现的，一律用 YAML。** 内置的 200 个通用漏洞已从 Go POC 迁移为 YAML 模板。只有 YAML 声明式语法表达不了的复杂算法场景，才值得用 Go 热加载。

| 维度 | YAML 模板 | Go 热加载 | WASM 模板 |
|---|---|---|---|
| 执行方式 | 原生 Go 引擎，只 Unmarshal 一次 | 每次执行都要过一遍解释器（编译 + 反射） | wazero 运行时 |
| 性能 | 最快 | **比 YAML 慢一个量级** | 中等（编译一次可复用） |
| 表达能力 | 声明式：请求序列 + 匹配 + 正则 + 表达式 + 耗时 + 反连 | **图灵完备**，可写任意循环/解析/算法 | 图灵完备，可跨语言 |
| 分发 | 单文件 | 单文件（控制器明文 / 节点密文） | 二进制 + 签名 |
| 维护成本 | 低 | 高 | 高 |
| 门槛 | 无 | 会 Go 即可，无需工具链 | 需构建链 + 签名 |
| 签名管控 | 无（走指纹同步） | 无（走指纹同步） | **硬门槛：无签名不执行** |
| 建议 | **首选** | 复杂算法/自有解析才用 | 需二进制分发或强签名时用 |

典型该用 Go 的场景：需要自己实现签名校验、自定义序列化、多轮状态机、位运算解析二进制协议、动态拼装非常规请求体等。**如果只是"请求 + 正则匹配"，请回去写 YAML。**

---

## 二、源码骨架与 `@meta` 注释头

### 2.1 `//go:build ignore` 是首行，不是可选

```go
//go:build ignore

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

`//go:build ignore` 必须是**文件第一行**（紧接着一个空行）。原因：这些明文模板会随源码树一起存在于工程目录中，如果没有构建约束，`go build ./...`、`go vet ./...` 会把它们当作正常源码编译，后果是编译报错或把你本不该编译的脚本打进二进制。加上 `ignore` 后 Go 工具链跳过它，而 **解释器不看构建标签，照样解释执行**。

由控制器按元数据生成的骨架会自动带上这一行；手工新建文件时千万别漏。

### 2.2 `@meta` 语法与解析正则

控制器与扫描节点解析 `@meta` 用的是**同一套**正则（两端行为一致）：

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

拆解一下（够用即可，不必背）：

- `^` 加 `(?m)`：每一行独立匹配，注释必须在**行首**（允许前置空格缩进），行中间出现的 `// @meta:` 不识别；
- `//\s*@meta:`：`//` 与 `@meta:` 之间允许空格；
- `([A-Za-z]\w*)`：键名，必须字母开头，后面可跟字母/数字/下划线；
- `(?:\.([A-Za-z0-9_-]{1,16}))?`：可选的语言后缀，如 `.en`；
- `(\+)?`：可选的多行续写标记；
- `\s*=\s*(.*?)\s*$`：等号，值取到行尾并去首尾空白。

四种写法：

| 写法 | 含义 |
|---|---|
| `// @meta:Name=SQL 注入检测` | 普通键值 |
| `// @meta:Description+=第二行内容` | **多行续写**，以 `\n` 追加到该键已有值 |
| `// @meta:Name.en=SQL Injection Detection` | **语言后缀**，仅语言相关键识别 |
| `// @meta:DefaultLanguage=cn` | 基准语言，缺省为 `cn` |

> [!WARNING]
> 键名大小写敏感、必须精确拼写；`@meta:` 写成 `@Meta:`、`@ meta:`、`meta:` 都会静默失效，表现为"漏洞名为空"。值分隔的 `=` 两侧空白会被自动去掉。

### 2.3 支持的键全表

`@meta` 注释头能映射到的模板元数据键如下（逐个取值）：

| 键 | 类型 | 必填 | 默认 | 示例值 | 写错的后果 |
|---|---|---|---|---|---|
| `Name` | string | 是（或 `VulnName`） | 空 | `SQL 注入检测` | 漏洞名为空，列表里显示空白条目 |
| `VulnName` | string | 否 | 空 | `SQL 注入检测` | 仅作 `Name` 的别名；两者都缺则名称为空 |
| `CVEId` | string | 否 | 空 | `CVE-2024-0001` | 卡片不显示 CVE 编号 |
| `CweId` | string | 否 | 空 | `CWE-89` | 卡片不显示 CWE 分类 |
| `CvssScore` | string | 否 | 空 | `9.8` | 卡片不显示 CVSS 评分 |
| `CvssVector` | string | 否 | 空 | `CVSS:3.1/AV:N/...` | 卡片不显示向量串 |
| `Level` | string | 否 | 空 | `high` | 空值或拼错 → 上报时按低危处理 |
| `Description` | string（语言相关） | 否 | 空 | `检测 SQL 报错特征` | 卡片"详情"为空 |
| `Solution` | string（语言相关） | 否 | 空 | `使用参数化查询` | 卡片"修复建议"为空 |
| `Author` | string | 否 | 空 | `admin` | 作者栏为空 |
| `References` | string | 否 | 空 | `https://example.com/advisory` | 参考链接为空（多行用 `+=`） |
| `Fingerprint` | string | 否 | 空 | `example-product` | 指纹为空 |
| `AffectedProducts` | string（语言相关） | 否 | 空 | `Example Product 1.0` | 受影响产品为空 |
| `Verification` | string | 否 | 空 | `手动复现步骤` | 验证说明为空 |
| `TestDepth` | string | 否 | `pack` | `pack`/`single-dir`/`single-domain` | 空则回退 `pack` |
| `Confidence` | int | 否 | `80` | `90` | 非法或 ≤0 时用 80 |
| `Enabled` | bool | 否 | `true` | `false` | 值为 `false`（忽略大小写）时模板停用 |
| `DefaultLanguage` | string | 否 | `cn` | `cn` | 空则回退平台默认基准语言 `cn` |

一份把所有键都写上的注释头长这样（仅作对照，非必填项可省略）：

```go
//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
```

**语言相关键只有 4 个**：`Name`、`Description`、`Solution`、`AffectedProducts`。只有它们支持 `键.语言码` 后缀。

### 2.4 多语言聚合规则

> [!CAUTION]
> 四端语言包同步是产品界面/日志文案的硬性要求；这里说的是 **POC 模板文案**，两者是不同体系，不要混用。POC 模板的多语言走本节的 `Languages` 机制。

- **无后缀键** → 进基础表，随后作为**基准语言**的文本放进 `Languages[DefaultLanguage]`；
- **带语言后缀键**（`Name.en` 等）→ 进 `langs[语言码]`，各自成块；
- 最后把平面字段回填为基准语言文本（口径与 YAML POC 完全一致）。

```go
// @meta:Name=SQL 注入检测
// @meta:Name.en=SQL Injection Detection
// @meta:Description=检测 SQL 报错特征
// @meta:Description.en=Detect SQL error patterns
// @meta:DefaultLanguage=cn
```

上述写法会得到 `Languages["cn"]` 与 `Languages["en"]` 两个完整语言块。**新增模板必须同时提供 `cn` 与 `en`**，否则英文界面下会显示中文。

非语言相关键若写了后缀（如 `References.en=...`），后缀不会被识别，键名会原样保留成 `References.en`，等于这个值丢了——别这么写。

### 2.5 完整可复制的 POC 源码骨架

下面这份骨架带 `//go:build ignore`、完整 `@meta` 头、`package main`、`func main()`，可直接新建文件粘进去改：

```go
//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),
		})
	}
}
```

> [!TIP]
> 把这份骨架当作"抄作业模板"：改 `@meta` 头的名称/等级/语言，再把 `main()` 里的判定逻辑换成你的检测算法即可。第五章每个符号都能单独抄进 `main()`。

### 2.6 最小可运行骨架逐行讲解

| 行 | 作用 | 写错的后果 |
|---|---|---|
| `//go:build ignore` | 让 `go build ./...` 跳过该文件 | 编译报错或被打进二进制 |
| `// @meta:Name=示例漏洞` | 漏洞名称（必填） | 名称为空，列表显示空白 |
| `// @meta:Name.en=...` | 英文名称 | 英文界面显示中文 |
| `// @meta:DefaultLanguage=cn` | 基准语言 | 回退 `cn` |
| `package main` | 合法 Go 源文件标志 | 加载时被静默跳过（模板"消失"） |
| `import "scan"` | 拿到全部注入符号 | 未 import 则 `scan.` 全部未定义 |
| `func main()` | **唯一入口**，解释器自动执行 | 没有 `main()` 则代码不跑 |

---

## 三、入口在哪：从 POC 模版列表到 `func main()` 被执行

这一章回答第三方开发者最常见的疑问："我写的这段代码，到底是被谁、在哪一步、怎么调起来的？"

### 3.1 全链路分步

1. **在 GUI「添加漏洞」里选 Go 类型**（界面标签页为「Go模板」），把源码粘进编辑器并保存；或者由 AI 渗透代理通过 `generate_poc` 工具提交。AI 提交的 POC 会被**强制停用（`Enabled=false`）**并标记作者来源，需人工复核后启用。
2. **控制器落盘明文源码**：保存后由控制器写到运行目录的 `poc/go/<VulnIde>.go`；若只填了元数据没写源码，会按元数据生成带 `@meta` 头与 `package main` 的骨架再落盘，并登记进内存模板列表。
3. **控制器启动/热加载注册**：控制器启动时保证 `poc/go` 目录存在，随后扫描该目录下的 `*.go`：没有 `package` 声明的文件被跳过；其余解析 `@meta` 头得到模板元数据（类型标记为 Go），归一化多语言字段后登记进模板列表。
4. **任务下发到扫描节点**：控制器把选中的模板（含源码文本）通过 TCP 下发给扫描节点。节点以 `package` 声明或 `@meta:` 判定这是 Go POC，用全局 AES 密钥加密后写到 **`scan-poc/go/<VulnIde>.gopoc`（密文）**，并在内存模板表注册（Go POC 不参与 yaml 指纹同步）。
5. **节点启动/重载时加载本地密文**：节点启动时保证 `scan-poc/go` 目录存在，先把历史 `.go` 密文自动改名为 `.gopoc`；逐个读取本地文件时**先 AES-GCM 解密、失败按明文处理**，能解析出 `package` 声明才受理，解析 `@meta` 后登记进本地模板表。
6. **任务执行时把源码交给解释器**：扫描命中某条 `PocType == "go"` 的模板时，节点构造好本次运行环境（目标流量、超时、代理等），把你的源码交给解释器执行。
7. **解释器自动执行 `func main()`**：解释器已预先注册 Go 标准库与平台自己的 `scan` 符号包，随后在受控的 goroutine 里解释你的源码。**解释器对含 `func main()` 的源码会自动执行 `main`，无需显式调用 `main.main()`**。
8. **你的代码通过 `import "scan"` 调宿主符号**：`scan.Flow` / `scan.HTTP()` / `scan.Report()` / `scan.Log()` 等，全部由平台在执行前注入好，你只需按第五章的签名调用。
9. **`scan.Report` 回传命中**：节点收集本次运行的全部 finding，把每条组装为漏洞明细上报控制器入库；同时把日志与 HTTP 请求数一并回传。

### 3.2 入口就是 `func main()`

- **入口只有 `func main()`**：没有 `_start`、没有导出函数、不需要注册任何回调。解释器拿到含 `func main()` 的源码后，会在解释执行时自动调用它。
- 你在 `main()` 里顺序写的请求就是**在同一次运行内顺序执行**的；共享 Cookie 会话（第五章/第六章）让"登录 → 访问"天然串起来。
- `main()` 返回即本次 POC 结束；收集到的 findings 与 logs 随本次执行结果交回节点。

### 3.3 标准库可用范围

执行引擎已预先注册 Go 标准库，因此 POC 里**可以 `import` Go 标准库**，例如：

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

常用可用包：`fmt`、`strings`、`strconv`、`bytes`、`regexp`、`encoding/json`、`encoding/base64`、`encoding/hex`、`net/url`、`net/http`、`crypto/*`、`time`、`sort`、`math` 等。

> [!WARNING]
> **只能 `import "scan"` 与已注册的标准库**。第三方包（如 `github.com/...`）没有注册，import 会解释失败。绝大多数场景直接用 `scan` 包（第 5 章）即可，不必引第三方。

### 3.4 `.gopoc` 扩展名的由来

节点本地存储密文用的是 `.gopoc` 扩展名。**不能叫 `.go`**：旧实现把 AES 密文写成 `scan-poc/go/<VulnIde>.go`，密文里看不到 `//go:build ignore`，Go 工具链把该文件当源码编译，节点工程 `go build ./...` 直接报 `illegal character U+00B0`。修复后：本地密文统一 `.gopoc`，历史 `.go` 密文由节点启动时自动改名迁移。

---

## 四、加载与执行链路

### 4.1 控制器侧：扫描目录 → 解析 `@meta` 注册

```text
控制器启动/热加载时：
  1. 保证运行目录下的 poc/go 存在
  2. 遍历 poc/go/*.go，逐个读取源码
  3. 源码必须含 package 声明，否则静默跳过
  4. VulnIde = 文件名去扩展名；解析 @meta 头得到元数据
  5. 归一化多语言字段后登记进模板列表（类型标记为 Go）
```

要点：

- 没有 `package` 声明的文件**被静默跳过**——不报错，只是"模板不见了"；
- `VulnIde` 直接取文件名。文件名重复会互相覆盖，务必全局唯一；
- 该扫描在控制器启动时执行一次；后续保存的新模板由控制器直接写盘并在内存登记。

### 4.2 扫描节点侧：迁移 → 加载 → 解密

```text
节点启动/重载时：
  1. 保证本地扫描目录下的 go 子目录存在
  2. 历史 .go 密文自动改名 .gopoc（best-effort）
  3. 遍历 *.gopoc 并兼容历史 *.go，逐个读取
  4. 先 AES-GCM 解密，失败按明文处理
  5. 能解析出 package 声明才受理，按 VulnIde 登记（重名去重，新扩展名优先）
```

### 4.3 执行：进程内解释

> [!NOTE]
> **实现原理（用的是哪个 Go 库）**：热加载执行引擎基于开源 Go 解释器库 **traefik/yaegi**（MIT 许可），
> 由扫描节点在进程内解释执行你的源码，因此你写的是**标准 Go 语法**、目标机**不需要 Go 工具链**。
> 这是一项普通的技术选型（选它是因为它完整支持 Go 语法，并能把平台自己的 `scan` 符号包注入进去），
> 与任何其它安全产品无关；对使用者完全透明——你只管用标准 Go 写 POC，不需要接触这个库。
> 本文其余部分统一用"解释器"指代它。

执行引擎在进程内解释你的源码，并预先把 **Go 标准库**与平台自己的 **`scan` 符号包**注册进去，因此你既能 `import` 标准库，也能 `import "scan"`。解释器会自动执行 `func main()`，本次运行产生的漏洞发现、调试日志与 HTTP 请求数会被统一收集。

解释器的符号键格式是 `importpath/packagename`，所以宿主注册的是 `"scan/scan"`；POC 里写 `import "scan"` 后即可用 `scan.Flow` / `scan.Report()` 等。

一次执行的完整过程：

1. 构建本次运行环境（含共享 `http.Client` + Cookie 会话，见第六章）；
2. 在受控的 goroutine 里执行你的源码；
3. 施加超时保护（默认 30s，超时即失败返回）；
4. 返回本次运行的**漏洞发现、调试日志、HTTP 请求数与错误**。

> [!WARNING]
> 解释器**没有强制取消机制**。超时后本次调用立即返回错误，但那个解释执行的 goroutine 会留在后台直到自己结束。因此 POC 里绝不能写无界循环或长 `Sleep`，否则会累积后台 goroutine。

### 4.4 运行时参数

POC 执行时由平台固定下来的运行参数（你不需要配置，只需知道这些行为）：

| 参数 | 默认 | 说明 |
|---|---|---|
| 单次执行超时 | 30s | 同时用作 HTTP 客户端超时；超时即判定执行失败 |
| 代理 | 空（直连） | HTTP 代理地址，由任务级下发 |
| 证书校验 | 调试与任务均忽略证书错误 | 便于对自签名靶场做测试 |
| 重定向 | 默认不跟随 | 需要跟随时由调试/任务侧开启 |
| 响应体上限 | 1MB | 单次响应体读取上限，超出部分被截断 |

`scan.Config` 里实际有哪些键取决于调用路径：

- 扫描任务：`timeout`、`proxy`、`seed`、`taskIde`；
- GUI 调试（`RunGoPocTest`）：`timeout`、`proxy`；
- 统一测试验证（`RunPocTest` 的 go 分支）：`target`。

---

## 五、`scan` 注入符号逐个精讲

> [!NOTE]
> 以下所有符号都在 `import "scan"` 后通过 `scan.` 前缀访问。**小节标题里的签名就是可用的权威签名。** 字符串类函数对**字节**操作（多字节 UTF-8 的 `Len`/`Substr` 按字节计），中文场景请留意。

每个小节的读法固定为：**作用**（什么时候调它）→ **签名**（逐字给出）→ **参数表**（传什么/从哪来/示例值）→ **返回**（类型 + 结构体字段表 + 取不到值时的返回）→ **代码片段**（可直接粘进 POC）→ **注意事项**。

### 数据对象

#### 5.1 `scan.Flow`

**作用**：当前处理的请求流量（被动流量或测试报文包）。几乎所有 POC 都从它取"要打的目标"，不要硬编码绝对 URL。

**类型**：结构体指针（脚本里用 `scan.Flow.字段` 访问）。

**字段表**：

| 字段 | 类型 | 含义 | 谁填充、什么格式 |
|---|---|---|---|
| `URL` | `string` | 完整请求 URL | 复制自流量包里的原始 URL |
| `Method` | `string` | 请求方法 | 复制自流量包（如 `GET`/`POST`） |
| `Host` | `string` | 目标主机名 | 优先取流量包里的域名；为空时由 URL 解析出主机名（不含端口） |
| `Scheme` | `string` | 协议 | 先取流量包的 TLS 标记，再用 URL 解析出的协议覆盖，结果为 `http`/`https` |
| `Path` | `string` | 请求路径 | 由 URL 的路径部分填充 |
| `Query` | `string` | 查询参数 | 由 URL 的原始查询串填充（**不含 `?`**） |
| `Headers` | `map[string]string` | 请求头 | 由原始请求头文本解析而来，**key 已转小写**；取用写 `scan.Flow.Headers["user-agent"]` |
| `Body` | `string` | 请求体 | 复制自流量包的请求体并转为字符串 |
| `RawHeaders` | `string` | 原始请求头文本（多行） | 复制自流量包的原始请求头文本 |

**代码片段**：

```go
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,
	})
}
```

**注意事项**：

1. 没有可用流量时，你会拿到一个空的 `Flow`（`Headers` 是空 map，其余字段为空串），不会 panic。
2. `Headers` 的 key 一定是小写；写 `scan.Flow.Headers["User-Agent"]` 取不到值。
3. 需要"当前完整请求原文"时读 `RawHeaders` + `Body`，不要自己拼。

#### 5.2 `scan.Config`

**作用**：读取任务级配置（超时/代理/随机种子等）。不同调用路径注入的键不同，取值前一定要判空。

**类型**：`map[string]string`。

**取值表**：

| 键 | 出现场景 | 含义 | 示例值 |
|---|---|---|---|
| `timeout` | 扫描任务、GUI 调试 | 执行超时（秒） | `"30"` |
| `proxy` | 扫描任务、GUI 调试 | 代理地址 | `"http://127.0.0.1:8080"` |
| `seed` | 扫描任务 | 扫描速度/深度种子 | `"1"` |
| `taskIde` | 扫描任务 | 任务标识 | `"task-..."` |
| `target` | 统一测试验证（RunPocTest） | 目标 URL | `"https://target/"` |

**代码片段**：

```go
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"])
```

**注意事项**：

1. 直接 `scan.Config["不存在"]` 返回空串，不会报错；但代码要能容忍空值。
2. 同一个键在不同入口可能缺失（例如 `RunPocTest` 没有 `seed`），不要假定它一定存在。
3. `Config` 是只读用途，别往里写值（写了也不会影响宿主）。

### 类型

#### 5.3 `scan.VulnFinding`

**作用**：`scan.Report` 的参数，一条漏洞发现的完整描述。你决定"命中"后构造它。

**构造方式**：`scan.VulnFinding{字段: 值, ...}`（类型符号本身由宿主注入，字段名必须精确）。

**字段表**：

| 字段 | 类型 | 含义 | 怎么用 |
|---|---|---|---|
| `Name` | `string` | 漏洞名称 | 留空则用模板 `@meta:Name`；填了会覆盖卡片标题，并清空模板多语言快照 |
| `Detail` | `string` | 漏洞详情 | 留空用模板描述；填了覆盖卡片"详情" |
| `Evidence` | `string` | 命中证据（响应片段/报文文本） | 没填 `Request`/`Response` 时作为"响应"展示 |
| `Level` | `string` | 等级 `critical`/`high`/`medium`/`low` | 映射 4/3/2/1；空或未识别回退模板等级，模板也空则低危 |
| `URL` | `string` | 命中 URL | 留空用当前 `Flow.URL` |
| `CVEId` | `string` | CVE 编号 | 留空用模板 `@meta:CVEId`；填了覆盖 |
| `Request` | `string` | 触发请求报文（文本） | 与 `Response` 一起作为请求-响应对展示 |
| `Response` | `string` | 命中响应报文（文本） | 同上 |
| `SensitiveText` | `string` | 敏感信息原文片段（命中内容前后各 200 字符） | 仅敏感信息类模板使用，GUI 高亮显示 |
| `SensitiveKeywords` | `[]string` | 实际命中的关键字列表 | GUI 纯文本查看器据此高亮 |
| `SensitiveMatchRule` | `string` | 实际匹配的公式/规则 | 敏感信息类模板记录匹配依据 |

**代码片段**：

```go
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),
})
```

**注意事项**：

1. `Level` 只用四个小写值 `critical`/`high`/`medium`/`low`；写 `HIGH` 或 `高危` 会被当成未识别。
2. `SensitiveText` / `SensitiveKeywords` / `SensitiveMatchRule` 是 **vuln-000007 敏感信息高亮专用**，普通 POC 不必填。
3. 一次 `main()` 里可以 `Report` 多条；每条都会独立生成一张漏洞卡片（受 404 基线误报闸门过滤，见第七章）。

#### 5.4 `scan.HTTPResponse`

**作用**：所有 HTTP 调用（`scan.HTTP` 及各便捷函数）的返回值类型。判断请求是否成功、取响应体/头/Cookie 都靠它。

**字段表**：

| 字段 | 类型 | 含义 | 怎么用 |
|---|---|---|---|
| `StatusCode` | `int` | HTTP 状态码 | **0 表示请求失败**（此时看 `Error`）；正常取 200/302/404 等 |
| `Body` | `string` | 响应体文本 | 受 1MB 上限裁剪（`MaxBodySize`） |
| `Headers` | `map[string]string` | 响应头，key 小写 | `resp.Headers["content-type"]` |
| `Cookies` | `[]string` | 响应 `Set-Cookie` 原始列表 | `len(resp.Cookies) > 0` 判断是否下发会话 |
| `URL` | `string` | 最终请求 URL（含重定向后） | 上报时用 `resp.URL` 更准确 |
| `TimeMs` | `int64` | 请求耗时（毫秒） | 时间盲注类判定 |
| `RawHeaders` | `string` | 原始响应头文本（多行） | 与 `Body` 拼成响应报文展示 |
| `Error` | `string` | 请求失败的错误信息（成功为空） | **先判 `Error` 再判 `StatusCode`** |

**代码片段**：

```go
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)))
```

**注意事项**：

1. 请求失败时 `StatusCode` 为 0、`Body` 为空、`Error` 非空；必须先判 `Error`。
2. `Headers` 的值只取同名头的**第一个值**，且 key 全部小写。
3. `Body` 最多读到 `MaxBodySize`（默认 1MB），超出部分静默截断。
4. `scan` 包没有注入 `Itoa`；要把数字拼进日志用 `scan.JSONDump(数字)`（返回 `"123"`）。

### 核心

#### 5.5 `scan.Log(msg string)`

**作用**：输出调试日志。这是排查"POC 到底走到哪一步"的唯一手段；GUI 调试面板与节点日志都能看到。

**签名**：

```go
scan.Log(msg string)
```

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `msg` | `string` | 任意调试文本 | 常量、`scan.Flow.URL`、`resp.Body` 片段等 | `"开始检测"` |

**返回**：无返回值。日志被追加进本次运行的日志缓冲，随执行结果一并交回节点。

**代码片段**：

```go
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))
```

**注意事项**：

1. 不要打印超大响应体（例如整个 `resp.Body` 到日志），会撑爆调试面板与 TCP 载荷；用 `scan.Substr` 截断。
2. `scan.Log` 不会中断执行，纯输出。
3. 日志顺序即代码顺序，可用来验证分支走向。

#### 5.6 `scan.Report(v VulnFinding)`

**作用**：上报一条漏洞发现。这是 POC 的"命中出口"——不调用它，跑得再对也不会产出漏洞卡片。

**签名**：

```go
scan.Report(v VulnFinding)
```

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `v` | `scan.VulnFinding` | 一条发现 | 用结构体字面量构造（字段见 5.3） | `scan.VulnFinding{Name:"...", Level:"high"}` |

**返回**：无返回值。每条 finding 被追加进本次运行的结果集。

**代码片段**：

```go
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("未命中")
}
```

**注意事项**：

1. `Level` 必填且只认四个小写值；留空会回退模板等级。
2. 若发现响应页与任务 404 基线相似度超阈值，会被误报闸门**直接丢弃**（不落库）。
3. `Name` / `Detail` 一旦填写，卡片将使用你的单语言文本，模板的多语言快照失效（见第七章）。

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

**作用**：发起一次任意方法的 HTTP 请求，走共享 Cookie 会话。需要自定义方法/请求头/请求体时用它。

**签名**（逐字来自平台注入的权威符号）：

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

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `method` | `string` | HTTP 方法；空串按 `GET` 处理 | 常量或 `scan.Flow.Method` | `"POST"` |
| `reqURL` | `string` | 完整 URL | `scan.Flow.URL`、由 `scan.URLHost/URLScheme` 拼出 | `"https://t/api/login"` |
| `headers` | `map[string]string` | 自定义请求头；nil 表示不加 | map 字面量 | `map[string]string{"Content-Type":"application/json"}` |
| `body` | `string` | 请求体；空串表示无体 | 常量或拼装字符串 | `"id=1' AND 1=1--"` |

**返回**：`scan.HTTPResponse`（字段见 5.4）。失败时 `Error` 非空、`StatusCode=0`、`Body` 空串。

**代码片段（自定义请求头 + Cookie 会话，先登录再访问受保护接口）**：

```go
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),
	})
}
```

**注意事项**：

1. `method == ""` 时底层按 `GET` 处理；但显式写方法更清晰。
2. 同名头会覆盖共享 jar 里的 Cookie；要"先登录再访问"就**不要**手写同名 Cookie 头。
3. `headers` 传 `nil` 法，遍历时不会 panic。

### 便捷 HTTP

#### 5.8 `scan.HTTPGet(rawurl string) scan.HTTPResponse`

**作用**：发一个 GET 请求。最常用的探测入口，等价于 `scan.HTTP("GET", rawurl, nil, "")`。

**签名**：

```go
scan.HTTPGet(rawurl string) scan.HTTPResponse
```

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `rawurl` | `string` | 完整 URL | `scan.Flow.URL`、拼接出的目标 | `"https://target/"` |

**返回**：`scan.HTTPResponse`（字段见 5.4）。

**代码片段**：

```go
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,
	})
}
```

**注意事项**：

1. 不自动加任何请求头（`headers` 传 `nil`）；需要特定头改用 `scan.HTTP` / `scan.HTTPHeader`。
2. 会自动带共享 Cookie 会话。
3. 不跟随重定向（除非任务/调试把 `AllowRedirects` 设为 true）。

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

**作用**：发一个 POST 请求。等价于 `scan.HTTP("POST", rawurl, nil, body)`。

**签名**：

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

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `rawurl` | `string` | 完整 URL | 拼接的目标 | `"https://target/api"` |
| `body` | `string` | 请求体 | 表单串或任意文本 | `"user=admin&pass=admin"` |

**返回**：`scan.HTTPResponse`（字段见 5.4）。

**代码片段**：

```go
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,
	})
}
```

**注意事项**：

1. **不自动设置 `Content-Type`**；表单通常要自己带（改用 `scan.HTTP` 显式加 `application/x-www-form-urlencoded`）。
2. `body` 为空串时表示无请求体。
3. 共享 Cookie 会话照常生效。

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

**作用**：发 JSON POST，自动加 `Content-Type: application/json`。登录/接口探测最常用。

**签名**：

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

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `rawurl` | `string` | 完整 URL | 拼接的目标 | `"https://target/api/login"` |
| `jsonBody` | `string` | JSON 文本 | 手写或 `scan.JSONDump(对象)` | `{"user":"admin"}` |

**返回**：`scan.HTTPResponse`（字段见 5.4）。

**代码片段**：

```go
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("拿到会话，继续")
}
```

**注意事项**：

1. 只加 `Content-Type`，不会自动把 map 序列化成 JSON；`jsonBody` 必须是合法 JSON 字符串。
2. 若目标要求其他头（如 `X-Token`），改用 `scan.HTTP` / `scan.HTTPHeader`。
3. 响应 `Set-Cookie` 会自动进共享 jar。

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

**作用**：`scan.HTTP` 的**别名**（宿主注入的就是同一个函数）；语义化写法，用于强调"要带自定义头"。

**签名**：

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

**参数表**：与 `scan.HTTP` 完全一致。

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `method` | `string` | HTTP 方法，空按 `GET` | 常量 | `"PUT"` |
| `reqURL` | `string` | 完整 URL | `scan.Flow.URL` | `"https://t/api"` |
| `headers` | `map[string]string` | 自定义请求头 | map 字面量 | `map[string]string{"Authorization":"Bearer x"}` |
| `body` | `string` | 请求体，空串无体 | 常量 | `""` |

**返回**：`scan.HTTPResponse`（字段见 5.4）。

**代码片段**：

```go
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)))
}
```

**注意事项**：

1. 它与 `scan.HTTP` 是**同一个函数**，行为完全一致，选哪个只看可读性。
2. 别同时写 `scan.HTTP` 和 `scan.HTTPHeader` 造成困惑，一个文件里保持一致即可。
3. 请求头 key 大小写由底层 `Header.Set` 规范化，不必纠结。

### 字符串

#### 5.12 `scan.ToLower(s string) string`

**作用**：转小写。做大小写无关匹配前的标准动作。

**签名**：`scan.ToLower(s string) string`（等价 `strings.ToLower`）。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `s` | `string` | 任意字符串 | `resp.Body` 等 | `"Root:X:0:0"` |

**返回**：`string`。空串进空串出。

**代码片段**：

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

**注意事项**：只做 Unicode 小写映射，不改变长度语义。

#### 5.13 `scan.ToUpper(s string) string`

**作用**：转大写。

**签名**：`scan.ToUpper(s string) string`（等价 `strings.ToUpper`）。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `s` | `string` | 任意字符串 | `resp.Body` | `"abc"` |

**返回**：`string`。

**代码片段**：

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

**注意事项**：与 `ToLower` 组合可做规范化比较。

#### 5.14 `scan.Trim(s string) string`

**作用**：去掉首尾空白（含换行/制表）。常用于清理响应片段或表单值。

**签名**：`scan.Trim(s string) string`（等价 `strings.TrimSpace`）。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `s` | `string` | 任意字符串 | 响应文本 | `"  admin\n"` |

**返回**：`string`（首尾空白删除后的结果）。

**代码片段**：

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

**注意事项**：只去首尾，不去中间空白。

#### 5.15 `scan.TrimCut(s, cutset string) string`

**作用**：去掉首尾**指定字符集**里的任意字符（注意不是整段子串）。

**签名**：`scan.TrimCut(s, cutset string) string`（等价 `strings.Trim`）。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `s` | `string` | 待裁剪字符串 | 响应文本 | `"///admin///"` |
| `cutset` | `string` | 要去掉的字符集合 | 常量 | `"/"` |

**返回**：`string`。

**代码片段**：

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

**注意事项**：

1. 第二个参数是"字符集"而非"子串"：`TrimCut(s, "ab")` 会去掉首尾所有 `a` 或 `b`。
2. 想按整段子串裁剪用 `scan.Replace(s, 子串, "")`。

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

**作用**：把 `s` 中**所有** `old` 替换成 `new`。

**签名**：`scan.Replace(s, old, new string) string`（等价 `strings.ReplaceAll`）。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `s` | `string` | 原字符串 | 响应文本 | `"a-b-c"` |
| `old` | `string` | 要被替换的内容 | 常量 | `"-"` |
| `new` | `string` | 替换成什么 | 常量 | `"_"` |

**返回**：`string`。

**代码片段**：

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

**注意事项**：`old` 为空串时，会在每个字符间插入 `new`（与 `strings.ReplaceAll` 一致），通常不是你要的。

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

**作用**：按分隔符拆分成切片。

**签名**：`scan.Split(s, sep string) []string`（等价 `strings.Split`）。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `s` | `string` | 原字符串 | 响应文本 | `"a,b,c"` |
| `sep` | `string` | 分隔符 | 常量 | `","` |

**返回**：`[]string`；`sep` 为空时按字符拆分。

**代码片段**：

```go
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]))
}
```

**注意事项**：

1. 解释执行下用**下标 + `len()` 循环**最稳，避免 `range` 带来的类型推断差异。
2. 用 `scan.Join` 可把切片拼回去。

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

**作用**：用分隔符把字符串切片拼成一个字符串。

**签名**：`scan.Join(elems []string, sep string) string`（等价 `strings.Join`）。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `elems` | `[]string` | 字符串切片 | `scan.Split` 的结果、`scan.RegexExtractAll` 等 | `[]string{"a","b"}` |
| `sep` | `string` | 分隔符 | 常量 | `","` |

**返回**：`string`。

**代码片段**：

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

**注意事项**：`elems` 为 nil/空切片时返回空串。

#### 5.19 `scan.Contains(s, substr string) bool`

**作用**：判断 `s` 是否包含子串。命中判定的高频函数。

**签名**：`scan.Contains(s, substr string) bool`（等价 `strings.Contains`）。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `s` | `string` | 被搜索字符串 | `resp.Body` | `"hello world"` |
| `substr` | `string` | 要找的子串 | 常量 | `"world"` |

**返回**：`bool`（包含为 true）。空 `substr` 恒为 true。

**代码片段**：

```go
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)})
}
```

**注意事项**：大小写敏感；无关匹配请先 `scan.ToLower`。

#### 5.20 `scan.HasPrefix(s, prefix string) bool`

**作用**：判断前缀。

**签名**：`scan.HasPrefix(s, prefix string) bool`（等价 `strings.HasPrefix`）。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `s` | `string` | 被检查字符串 | `resp.Body` | `"http://x"` |
| `prefix` | `string` | 前缀 | 常量 | `"http"` |

**返回**：`bool`。

**代码片段**：

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

**注意事项**：空 `prefix` 恒为 true。

#### 5.21 `scan.HasSuffix(s, suffix string) bool`

**作用**：判断后缀。

**签名**：`scan.HasSuffix(s, suffix string) bool`（等价 `strings.HasSuffix`）。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `s` | `string` | 被检查字符串 | 路径 | `"/admin/"` |
| `suffix` | `string` | 后缀 | 常量 | `"/"` |

**返回**：`bool`。

**代码片段**：

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

**注意事项**：空 `suffix` 恒为 true。

#### 5.22 `scan.Index(s, substr string) int`

**作用**：返回子串首次出现的字节下标。找不到返回 `-1`。

**签名**：`scan.Index(s, substr string) int`（等价 `strings.Index`）。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `s` | `string` | 被搜索字符串 | `resp.Body` | `"abcabc"` |
| `substr` | `string` | 子串 | 常量 | `"bc"` |

**返回**：`int`；**找不到返回 `-1`**（不会 panic）。

**代码片段**：

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

**注意事项**：返回的是**字节**下标，中文场景下不等于字符位置。

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

**作用**：截取子串 `s[start:end]`，越界自动裁剪。日志/证据截断的标配。

**签名**：`scan.Substr(s string, start, end int) string`。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `s` | `string` | 原字符串 | `resp.Body` | `"abcdef"` |
| `start` | `int` | 起始下标（含） | 常量 | `0` |
| `end` | `int` | 结束下标（不含） | 常量 | `3` |

**返回**：`string`。裁剪规则：`start<0` → 归 0；`end>len(s)` → 归 `len(s)`；`start>=end` → 返回**空串**。

**代码片段**：

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

**注意事项**：

1. 按**字节**截取，可能在多字节 UTF-8 字符中间切断；中文内容请宽松些。
2. `start >= end` 或越界到空区间时返回空串，不 panic。

#### 5.24 `scan.Len(s string) int`

**作用**：取字符串字节长度。

**签名**：`scan.Len(s string) int`。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `s` | `string` | 任意字符串 | `resp.Body` | `"hello"` |

**返回**：`int`（**字节数**，不是字符数）。

**代码片段**：

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

**注意事项**：中文一个汉字通常 3 字节，`Len("中文")==6`。

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

**作用**：把字符串重复 `count` 次。

**签名**：`scan.Repeat(s string, count int) string`（等价 `strings.Repeat`）。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `s` | `string` | 原字符串 | 常量 | `"A"` |
| `count` | `int` | 次数 | 常量 | `5` |

**返回**：`string`。

**代码片段**：

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

**注意事项**：

1. `count` 为负会 panic（与 `strings.Repeat` 一致），确保非负。
2. 别构造过大字符串，会占用执行时间与内存。

### 编解码 / 哈希

#### 5.26 `scan.Base64Encode(s string) string`

**作用**：标准 Base64 编码（常用于构造 Basic 认证、编码 payload）。

**签名**：`scan.Base64Encode(s string) string`（`base64.StdEncoding.EncodeToString`）。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `s` | `string` | 原文 | 凭据、payload | `"admin:admin"` |

**返回**：`string`（Base64 文本）。

**代码片段**：

```go
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))
```

**注意事项**：使用标准字符表（含 `+ / =`），不是 URL-safe 变体。

#### 5.27 `scan.Base64Decode(s string) string`

**作用**：Base64 解码；解码失败返回空串。

**签名**：`scan.Base64Decode(s string) string`。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `s` | `string` | Base64 文本 | 响应中的编码字段 | `"YWRtaW4="` |

**返回**：`string`；**非法 Base64 返回空串**（不报错）。

**代码片段**：

```go
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)})
}
```

**注意事项**：只接受标准字符表；URL-safe（`-_`）会失败返回空串。

#### 5.28 `scan.URLEncode(s string) string`

**作用**：URL 查询转义（把 `& / 空格` 等编码）。

**签名**：`scan.URLEncode(s string) string`（等价 `url.QueryEscape`）。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `s` | `string` | 原文 | 参数值 | `"a b&c"` |

**返回**：`string`。

**代码片段**：

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

**注意事项**：`QueryEscape` 会把空格编成 `+`；路径段编码应改用其他方式。

#### 5.29 `scan.URLDecode(s string) string`

**作用**：URL 反转义；失败返回空串。

**签名**：`scan.URLDecode(s string) string`。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `s` | `string` | 转义串 | 响应/URL | `"a%20b"` |

**返回**：`string`；**非法转义返回空串**。

**代码片段**：

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

**注意事项**：能正确处理 `+` 为空格（`QueryUnescape` 语义）。

#### 5.30 `scan.HexEncode(s string) string`

**作用**：十六进制编码。

**签名**：`scan.HexEncode(s string) string`。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `s` | `string` | 原文 | 任意 | `"abc"` |

**返回**：`string`（小写十六进制）。

**代码片段**：

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

**注意事项**：输出为小写。

#### 5.31 `scan.HexDecode(s string) string`

**作用**：十六进制解码；失败返回空串。

**签名**：`scan.HexDecode(s string) string`。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `s` | `string` | 十六进制文本 | 响应字段 | `"616263"` |

**返回**：`string`；**长度为奇数或含非法字符时返回空串**。

**代码片段**：

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

**注意事项**：仅接受偶数长度的十六进制串。

#### 5.32 `scan.MD5(s string) string`

**作用**：计算 MD5 十六进制小写摘要（弱哈希，用于签名/指纹比对，不是安全场景）。

**签名**：`scan.MD5(s string) string`。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `s` | `string` | 原文 | 任意 | `"admin"` |

**返回**：`string`（32 位小写十六进制）。

**代码片段**：

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

**注意事项**：MD5 仅用于校验/指纹，不可用于密码学安全。

#### 5.33 `scan.SHA1(s string) string`

**作用**：SHA1 十六进制小写摘要。

**签名**：`scan.SHA1(s string) string`。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `s` | `string` | 原文 | 任意 | `"abc"` |

**返回**：`string`（40 位小写十六进制）。

**代码片段**：

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

**注意事项**：与 `MD5`/`SHA256` 输出格式一致，均为小写十六进制。

#### 5.34 `scan.SHA256(s string) string`

**作用**：SHA256 十六进制小写摘要。

**签名**：`scan.SHA256(s string) string`。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `s` | `string` | 原文 | 任意 | `"abc"` |

**返回**：`string`（64 位小写十六进制）。

**代码片段**：

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

**注意事项**：对原始字节计算，不做任何规范化。

### 正则

#### 5.35 `scan.RegexMatch(s, pattern string) bool`

**作用**：判断正则是否匹配（不提取内容）。

**签名**：`scan.RegexMatch(s, pattern string) bool`。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `s` | `string` | 被搜索文本 | `resp.Body` | `"root:x:0:0"` |
| `pattern` | `string` | 正则表达式 | 常量 | `"(?i)root:x:0:0"` |

**返回**：`bool`；**正则非法时 `regexp.MatchString` 的 error 被忽略，返回 false**。

**代码片段**：

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

**注意事项**：**正则写错不会报错，只会永远不匹配**——调试时先用 `scan.Log` 打印待匹配文本。

#### 5.36 `scan.RegexExtract(s, pattern string) string`

**作用**：提取首个匹配；**pattern 含捕获组时返回第 1 组，否则返回整个匹配**。

**签名**：`scan.RegexExtract(s, pattern string) string`。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `s` | `string` | 被搜索文本 | `resp.Body` | `"user: admin"` |
| `pattern` | `string` | 正则（建议带捕获组） | 常量 | `"user:\s*(\w+)"` |

**返回**：`string`；**无匹配、正则非法、或第 1 组为空且无整体匹配时返回空串**。

**代码片段**：

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

**注意事项**：

1. 当有多个捕获组时只返回**第 1 组**（`m[1]`）。
2. 第 1 组匹配到空串时回退为整个匹配 `m[0]`。
3. 要拿多组信息就写多次 `RegexExtract`。

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

**作用**：提取全部匹配，用 `sep` 连接成一个字符串。

**签名**：`scan.RegexExtractAll(s, pattern, sep string) string`。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `s` | `string` | 被搜索文本 | `resp.Body` | `"a1 b2 c3"` |
| `pattern` | `string` | 正则 | 常量 | `"\d+"` |
| `sep` | `string` | 连接分隔符 | 常量 | `","` |

**返回**：`string`；**正则非法返回空串**；无匹配返回空串。

**代码片段**：

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

**注意事项**：返回的是**拼接后的字符串**，不是切片；需要切片请 `scan.Split(nums, ",")`。

### JSON

#### 5.38 `scan.JSONGet(jsonStr, path string) string`

**作用**：从 JSON 字符串按点路径取值。路径支持对象键与数组下标混用。

**签名**：`scan.JSONGet(jsonStr, path string) string`。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `jsonStr` | `string` | JSON 文本 | `resp.Body` | `{"data":{"role":"admin"}}` |
| `path` | `string` | 点路径，支持 `a.b.c` 与 `items.0.name` | 常量 | `"data.role"` |

**返回**：`string`。规则：字符串值原样返回；`null` → 空串；对象/数组 → **序列化回 JSON 字符串**；路径不存在或 JSON 非法 → 空串。

**代码片段**：

```go
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)})
}
```

**注意事项**：

1. 数字值取出来也是字符串（如 `"1"`），比较时注意加引号。
2. 路径里的空段会被 `Trim(path, ".")` 去掉首尾点。
3. 要取嵌套对象整体，`JSONGet` 会返回其 JSON 文本。

#### 5.39 `scan.JSONValid(s string) bool`

**作用**：判断字符串是否为合法 JSON。

**签名**：`scan.JSONValid(s string) bool`（`json.Valid`）。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `s` | `string` | 待检查文本 | `resp.Body` | `{"ok":true}` |

**返回**：`bool`。

**代码片段**：

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

**注意事项**：只校验合法性，不解析结构；后续取值仍可能因路径不存在返回空串。

#### 5.40 `scan.JSONDump(v interface{}) string`

**作用**：把任意值序列化为 JSON 字符串。**也是把数字/布尔拼进日志的标准办法**（`scan` 没有注入 `Itoa`）。

**签名**：`scan.JSONDump(v interface{}) string`。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `v` | `interface{}` | 任意值 | map/切片/数字/布尔 | `map[string]int{"code":200}` |

**返回**：`string`；**序列化失败返回空串**。

**代码片段**：

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

**注意事项**：

1. 传数字会得到 `"200"`，传字符串会得到**带引号的** `"\"ok\""`——日志拼接时优先传数字/map，别传裸字符串。
2. 无法序列化的值（如函数）返回空串。

### 随机 / 时间

#### 5.41 `scan.RandInt(min, max int) int`

**作用**：生成 `[min, max)` 区间随机整数。

**签名**：`scan.RandInt(min, max int) int`。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `min` | `int` | 下界（含） | 常量 | `1` |
| `max` | `int` | 上界（不含） | 常量 | `100` |

**返回**：`int`；**`max<=min` 时按 `max=min+1` 处理**，即返回 `min`。

**代码片段**：

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

**注意事项**：使用 `crypto/rand`，并发安全；区间是左闭右开。

#### 5.42 `scan.RandString(n int) string`

**作用**：生成 n 位随机串（大小写字母 + 数字）。用作反连标记、防缓存参数最合适。

**签名**：`scan.RandString(n int) string`。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `n` | `int` | 期望长度 | 常量 | `12` |

**返回**：`string`；**`n<=0` 时按 8 位处理**。

**代码片段**：

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

**注意事项**：字符集为 `a-zA-Z0-9`；需要纯字母/纯数字用 `RandAlpha`/`RandNum`。

#### 5.43 `scan.RandAlpha(n int) string`

**作用**：生成 n 位随机**小写字母**串。

**签名**：`scan.RandAlpha(n int) string`。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `n` | `int` | 期望长度 | 常量 | `6` |

**返回**：`string`；**`n<=0` 时按 8 位处理**。

**代码片段**：

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

**注意事项**：字符集仅 `abcdefghijklmnopqrstuvwxyz`，不含大写。

#### 5.44 `scan.RandNum(n int) string`

**作用**：生成 n 位随机**数字**串。

**签名**：`scan.RandNum(n int) string`。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `n` | `int` | 期望长度 | 常量 | `6` |

**返回**：`string`；**`n<=0` 时按 8 位处理**。

**代码片段**：

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

**注意事项**：字符集仅 `0123456789`；首位可以是 `0`。

#### 5.45 `scan.UUID() string`

**作用**：生成 UUID v4。用作请求幂等键、临时资源名。

**签名**：`scan.UUID() string`（**无参数**）。

**参数表**：无参数。

**返回**：`string`，形如 `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`。

**代码片段**：

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

**注意事项**：每次调用生成新值（`crypto/rand`）。

#### 5.46 `scan.Timestamp() int64`

**作用**：取当前 Unix **秒**时间戳。

**签名**：`scan.Timestamp() int64`（**无参数**）。

**参数表**：无参数。

**返回**：`int64`（Unix 秒）。

**代码片段**：

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

**注意事项**：返回值是 `int64`；拼接日志用 `scan.JSONDump`。

#### 5.47 `scan.TimestampMs() int64`

**作用**：取当前 Unix **毫秒**时间戳。

**签名**：`scan.TimestampMs() int64`（**无参数**）。

**参数表**：无参数。

**返回**：`int64`（Unix 毫秒）。

**代码片段**：

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

**注意事项**：测耗时优先用 `resp.TimeMs`，`TimestampMs` 差值包含额外开销。

#### 5.48 `scan.Sleep(ms int)`

**作用**：休眠指定毫秒。用于反连轮询、时间盲注等待。

**签名**：`scan.Sleep(ms int)`（**无返回值**）。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `ms` | `int` | 休眠毫秒数 | 常量 | `500` |

**返回**：无。

**代码片段**：

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

**注意事项**：

1. **`Sleep` 占用执行时间预算**，总超时默认 30s；轮询要控制次数与单次时长。
2. 解释器无强制取消，长 `Sleep` 在超时后仍会留后台。

### URL 解析

#### 5.49 `scan.URLHost(rawurl string) string`

**作用**：取 URL 的主机名（**不含端口**）。

**签名**：`scan.URLHost(rawurl string) string`。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `rawurl` | `string` | 完整 URL | `scan.Flow.URL` | `"https://a.com:8443/x"` |

**返回**：`string`；**解析失败返回空串**。注意 `Hostname()` 会去掉端口。

**代码片段**：

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

**注意事项**：`URLHost` 返回的是主机名而非 `Host`（无端口）；需要带端口请自己解析。

#### 5.50 `scan.URLPath(rawurl string) string`

**作用**：取 URL 路径部分。

**签名**：`scan.URLPath(rawurl string) string`。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `rawurl` | `string` | 完整 URL | `scan.Flow.URL` | `"https://a.com/a/b?x=1"` |

**返回**：`string`（如 `/a/b`）；解析失败返回空串。

**代码片段**：

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

**注意事项**：返回路径**不含查询串**（查询用 `URLQuery`）。

#### 5.51 `scan.URLQuery(rawurl string) string`

**作用**：取原始查询串（**不含 `?`**）。

**签名**：`scan.URLQuery(rawurl string) string`。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `rawurl` | `string` | 完整 URL | `scan.Flow.URL` | `"https://a.com/a?x=1&y=2"` |

**返回**：`string`（如 `x=1&y=2`）；解析失败或无查询返回空串。

**代码片段**：

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

**注意事项**：返回**未解码**的原始串；要可读文本配合 `scan.URLDecode`。

#### 5.52 `scan.URLScheme(rawurl string) string`

**作用**：取协议（`http`/`https`）。

**签名**：`scan.URLScheme(rawurl string) string`。

**参数表**：

| 参数 | 类型 | 传什么 | 从哪来 | 示例值 |
|---|---|---|---|---|
| `rawurl` | `string` | 完整 URL | `scan.Flow.URL` | `"https://a.com/"` |

**返回**：`string`（`http` 或 `https`）；解析失败返回空串。

**代码片段**：

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

**注意事项**：拼接站点根时通常 `scan.URLScheme + "://" + scan.URLHost`。

#### 5.53 符号全表（权威清单）

> [!IMPORTANT]
> 下表是**权威符号清单**。POC 里出现的 `scan.xxx` 必须在这张表内；表外的符号（例如 `scan.Itoa`）**并不存在**，写了会解释报错。

| 分类 | 全部符号 |
|---|---|
| 数据 | `scan.Flow`、`scan.Config` |
| 类型 | `scan.VulnFinding`、`scan.HTTPResponse` |
| 核心 | `scan.Log`、`scan.Report`、`scan.HTTP` |
| 便捷 HTTP | `scan.HTTPGet`、`scan.HTTPPost`、`scan.HTTPJSONPost`、`scan.HTTPHeader` |
| 字符串 | `scan.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` |
| 编解码/哈希 | `scan.Base64Encode`、`scan.Base64Decode`、`scan.URLEncode`、`scan.URLDecode`、`scan.HexEncode`、`scan.HexDecode`、`scan.MD5`、`scan.SHA1`、`scan.SHA256` |
| 正则 | `scan.RegexMatch`、`scan.RegexExtract`、`scan.RegexExtractAll` |
| JSON | `scan.JSONGet`、`scan.JSONValid`、`scan.JSONDump` |
| 随机/时间 | `scan.RandInt`、`scan.RandString`、`scan.RandAlpha`、`scan.RandNum`、`scan.UUID`、`scan.Timestamp`、`scan.TimestampMs`、`scan.Sleep` |
| URL 解析 | `scan.URLHost`、`scan.URLPath`、`scan.URLQuery`、`scan.URLScheme` |

---

## 六、Cookie 会话语义

每次 POC 运行都会创建一个**共享的 `http.Client` + `net/http/cookiejar`**，所有 HTTP 调用（`scan.HTTP` / `scan.HTTPGet` / `scan.HTTPPost` / `scan.HTTPJSONPost` / `scan.HTTPHeader`）都走同一个客户端。因此：

- 第一步登录得到的 `Set-Cookie` 会被 jar 自动保存；
- 第二步请求同一站点时自动带上 Cookie；
- **"先登录，再访问受保护接口"两步链路天然成立**，无需手工搬运 Cookie。

```go
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)))
}
```

**headers 参数**：`scan.HTTP(method, reqURL, headers, body)` 的第三个参数是 `map[string]string`，用于设置请求头；`scan.HTTPHeader` 与它完全等价，只是名称更直白。若你手工设置了同名 Cookie 头，显式头会覆盖会话里的同名值——正常行为，但要"先登录"就别手写同名 Cookie。

> [!TIP]
> 三步以上的链路（登录 → 提权 → 拿数据）也能靠共享 jar 串起来；只要都在同一次运行的 `main()` 里顺序执行即可。

---

## 七、回传结果：`scan.Report` 与漏洞卡片

调用 `scan.Report(scan.VulnFinding{...})` 后，节点把每条 finding 组装成漏洞明细上报控制器入库。各字段对最终漏洞卡片的影响：

| `VulnFinding` 字段 | 对卡片的影响 |
|---|---|
| `Name` | 覆盖模板 `@meta:Name`，作为卡片标题；一旦覆盖，模板的多语言快照会被清空（POC 生成的名称是单语言文本） |
| `Detail` | 覆盖模板描述，作为卡片"详情"；同样清空多语言快照 |
| `Evidence` | 当没有填 `Request`/`Response` 时，作为请求-响应列表中的"响应"展示 |
| `Level` | 映射为漏洞等级：`critical`→4、`high`→3、`medium`→2、`low`→1；未识别或空值按低危处理；空则回退模板 `@meta:Level` |
| `URL` | 命中 URL；留空用当前 `Flow.URL` |
| `CVEId` | 覆盖模板 CVE 编号 |
| `Request` / `Response` | 作为请求-响应报文对展示 |
| `SensitiveText` / `SensitiveKeywords` / `SensitiveMatchRule` | 敏感信息高亮三字段，仅敏感信息类模板使用 |

> [!WARNING]
> 上报前还有一道 **404 基线误报闸门**：若发现响应页与任务 404 基线相似度超过阈值，会被判为误报**直接丢弃**。所以别把普通错误页当漏洞上报，也别让 `Level` 留空——空等级会被当低危。

---

## 八、本地开发与在线调试

### 8.1 在 GUI 里建模板

1. 打开漏洞配置，点击「添加漏洞」，**模板类型选 Go**（界面标签页为「Go模板」）；
2. 在编辑器里编写源码（含 `//go:build ignore` + `@meta` 头）；
3. 保存后控制器写入 `poc/go/<VulnIde>.go`，内存注册；下发节点时由节点 AES 加密为 `scan-poc/go/<VulnIde>.gopoc`。

`GoSource` 为空时，控制器会按元数据生成一份带 `@meta` 头与 `package main` 的骨架。

### 8.2 相关指令与数据结构

| 指令 | 方向 | 作用 |
|---|---|---|
| `RunGoPocTest` | GUI → 控制器 → 扫描节点 | 调试运行 Go 热加载 POC，回传 `GoPocTestResult`（findings + logs + HTTP 数） |
| `GetGoPocDetail` | GUI → 控制器 | 拉取某漏洞的完整详情（含 `GoSource`），编辑时用最新源码 |
| `PocTestRequest` / `RunPocTest` | GUI → 控制器 → 扫描节点 | 统一测试验证（yaml/go/wasm 共用），回传 `PocTestResult` |

`RunGoPocTest` 的请求结构 `GoPocTestRequest` 字段：`VulnIde`、`GoSource`、`TargetURL`、`TestFlow`、`Timeout`（秒，默认 30）、`Proxy`。结果结构 `GoPocTestResult`：`VulnIde`、`Success`、`Findings`、`Logs`、`HttpCount`、`Error`。

统一测试验证入口 `RunPocTest` 则用 `PocTestRequest.PocType` 分发；`PocType="go"` 时走同一套解释器执行，`Success` 与 `Found` 分开返回（`Found = len(findings) > 0`）。

### 8.3 测试流量包从哪来

调试时你可以不填流量包，只用 `TargetURL`（节点会自动构造一个 GET 流量）。更真实的做法是**粘贴 burp 风格的原始请求报文**：节点会解析"请求行 + 头 + 空行 + Body"，从 `Host` 头与请求行路径拼出完整 URL。GUI 也支持按 YAML 模板的请求步骤索引构造流量包。

### 8.4 静默测试

测试验证的 yaml 分支与调试入口都采用"只回传不落库"语义：命中详情从结果里读取，**不写入漏洞库**。AI 派单场景下 `WantExchanges=true` 还会回传每一步真实请求/响应原文（默认 8KB 截断），供 AI 自行判定。

> [!NOTE]
> `RunPocTest` 的 go 分支目前**不会把代理地址下发给执行引擎**（只把 `target` 放进 `Config`），而 `RunGoPocTest` 与扫描任务会下发。若你的目标只能经代理访问，走 GUI「Go模板」标签页的运行测试（`RunGoPocTest`）更稳妥。以 `RunPocTest` 走 go 调试时请留意这一差异。

---

## 九、调试手册

这一章是"跑不通时翻哪里"的速查。先讲三个观察面，再给 5 分钟最小验证流程，最后是症状对照表。

### 9.1 日志与结果在哪里看

| 观察面 | 看什么 | 数据来源 | 什么时候用 |
|---|---|---|---|
| GUI「Go模板」调试面板 | `scan.Log` 输出 + 命中 findings + HTTP 请求数 | `GoPocTestResult` 的 `Logs` / `Findings` / `HttpCount` | 开发期逐行验证逻辑，最快 |
| 任务结果漏洞卡片 | 漏洞名 / 详情 / 等级 / 请求-响应报文 | 节点上报的漏洞明细（入库后可查） | 验证真实扫描任务里的产出 |
| 控制器 / 节点日志 | POC 执行的失败、超时与逐条日志 | 节点 / 控制器日志 | 任务态排查、看 POC 是否被调度 |
| 统一测试验证结果 | `PocTestResult` 的 `Success` / `Found` / `Findings` | `RunPocTest` 的 go 分支 | AI 派单 / 测试验证标签页 |

### 9.2 五分钟最小验证流程

1. 打开 GUI 漏洞配置，点「添加漏洞」，**模板类型选 Go**，新建一个模板。
2. 在编辑器里填入最小骨架（可复制第 2.5 节的完整骨架，`@meta:Name` 随便起），保存。
3. 切到该模板的「Go模板」标签页，在"测试目标 URL"里填一个你能访问的地址（例如 `http://你的靶场/`）。
4. 点「运行测试」。GUI 会发 `RunGoPocTest` 指令 → 控制器转发给在线扫描节点 → 节点构造 Flow 并交给解释器执行。
5. 观察返回：
   - `GoPocTestResult.Logs` 里有你 `scan.Log` 打的每一行 → 代码确实执行了；
   - `GoPocTestResult.HttpCount > 0` → 请求确实发出去了；
   - 若 `Success=false`，`Error` 会写明是执行错误还是超时；
   - 命中时 `Findings` 非空，GUI 显示漏洞卡片。
6. 若第 5 步没反应：先看 `Error`，再看 `Logs` 是否为空。`Logs` 为空通常意味着 `main()` 根本没跑到（见 9.3 对照表）。

> [!TIP]
> 调试阶段把关键中间值都 `scan.Log` 出来（用 `scan.Substr` 截断），比盯着编辑器猜快得多。

### 9.3 症状 → 原因 → 解决

| 症状 | 原因 | 解决 |
|---|---|---|
| `go build ./...` 报 `illegal character U+00B0`，或源码树编译报错 | 忘写 `//go:build ignore`，模板被当正常源码编译 | 首行必须是 `//go:build ignore`，后跟一个空行；由平台生成的骨架已带 |
| POC 执行明显比同功能 YAML 慢 | 每次执行都要过一遍解释器（编译 + 反射），**慢一个量级** | 能用 YAML 一律用 YAML，Go 只用于 YAML 表达不了的复杂算法 |
| 执行报"解释失败/undefined: xxx" | `import` 了白名单外的第三方包 | 平台只注册了 Go 标准库与 `scan` 包；改用 `scan` 包或已注册的标准库 |
| 报错 `Go POC 执行超时（30s）` | 逻辑耗时超过单次执行超时（默认 30s）；请求数过多或 `Sleep` 过长 | 控制请求数与循环次数；缩短/去掉 `Sleep`；总预算不要逼近超时上限 |
| 漏洞卡片等级异常 / 显示为低危 | `Report` 的 `Level` 取值空或写错（如 `HIGH`/`高危`） | 固定用 `critical`/`high`/`medium`/`low` 四个小写值 |
| 节点 `go build ./...` 报密文编译错误 | 节点侧密文被写成 `.go`（历史事故，密文里看不到构建标签） | 节点存储用 `.gopoc`；节点启动时会自动迁移历史 `.go` 密文 |
| 英文界面下显示中文名称/描述 | 只写了 `cn`，没补 `.en` | 语言相关键（`Name`/`Description`/`Solution`/`AffectedProducts`）补齐 `.en` 后缀 |
| 漏洞名为空 / 元数据全部丢失 | `@meta:` 大小写或拼写错误、注释不在行首、写成了 `@Meta:`/`meta:` | 键名精确大小写；注释必须在行首；`=` 两侧空白会被自动去掉 |
| 整体超时，日志显示卡在轮询 | `scan.Sleep` 占用执行时间预算 | 控制单次时长与次数；轮询总时长留足余量 |
| 任务跑完后节点进程内存/协程持续增长 | POC 里写了无界循环，解释器在超时后**不能强制取消**，goroutine 滞留后台 | 所有循环必须有明确上界；避免 `for {}` |
| 模板在列表里"消失"，什么都没发生 | 缺 `package` 声明，加载时被静默跳过（不报错） | 必须有 `package main`（或合法 `package` 声明） |
| 命中不了但请求成功 | `Level` 之外，常见是正则写错（`RegexMatch` 非法正则返回 false 且不报错） | 用 `scan.Log` 打印待匹配文本；先用简单子串 `scan.Contains` 验证通路 |
| 只能经代理访问的目标直连失败 | `RunPocTest` 的 go 分支**未把代理下发给执行引擎**（已知限制） | 走 GUI「Go模板」的 `RunGoPocTest` 或真实扫描任务，它们会下发代理 |
| 响应体判定不到尾部特征 | 单次读取上限 1MB，超出被截断 | 只取前段做判定；需要全量改用多次分段请求 |
| 两步会话失效（第二步未登录） | 手工设置了与 `Set-Cookie` 同名的 Cookie 头，覆盖了会话值 | 需要"先登录"就别手写同名 Cookie；让共享 jar 自动管理 |
| 通用模板在别的任务里请求了写死的主机 | 代码里硬编码了绝对 URL | 用 `scan.Flow` 取当前流量；AI 生成链路会直接拒绝硬编码目标 |

> [!TIP]
> 写完模板先自查四件事：首行是不是 `//go:build ignore`？`@meta:Name` 是不是行首？有没有 `package main` 和 `func main()`？用到的 `scan.xxx` 是不是都在 5.53 符号全表里？这四点能挡掉绝大多数"模板不生效"的问题。

---

## 十、完整示例

> 所有示例都省略了 `//go:build ignore` 之后的空行细节，实际文件请保持首行构建约束 + 空行。

### 10.1 示例一：响应特征匹配（最简）

```go
//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),
		})
	}
}
```

### 10.2 示例二：两步 Cookie 会话（先登录，再访问受保护接口）

```go
//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 示例三：JSON 解析 + 正则提取 + 多分支上报

```go
//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)))
}
```

---

## 十一、注意事项清单

| 坑 | 表现 | 规避 |
|---|---|---|
| 忘写 `//go:build ignore` | 源码树里的模板被 `go build ./...` 编译，报错或被打包 | 首行必须是 `//go:build ignore`，后跟空行；由平台生成的骨架已带 |
| 期望 YAML 级性能 | 每次执行都要过一遍解释器（编译 + 反射），**慢一个量级** | 能用 YAML 一律用 YAML，Go 只用于复杂算法 |
| import 未注册的包 | `import` 第三方包 → 解释失败 | 平台只注册了 Go 标准库与 `scan`；扩展能力走 `scan` 包，别引第三方 |
| 超时 30s | 执行超过 30s 返回超时错误；且 解释器**不能真正取消** goroutine | 控制请求数与逻辑量；`scan.Sleep` 会占用执行时间预算 |
| 用了 `scan` 里不存在的符号 | 解释报 `undefined`（例如误写 `scan.Itoa`） | 只用 5.53 符号全表内的符号；数字转字符串用 `scan.JSONDump` |
| `Report` 的 `Level` 取值 | 空值或拼错 → 回退模板等级，模板也没写则按低危 | 固定用 `critical`/`high`/`medium`/`low` 四个小写值 |
| 节点侧扩展名必须 `.gopoc` | 密文写成 `.go` → 节点 `go build ./...` 报 `illegal character U+00B0`（历史事故） | 节点存储用 `.gopoc`；节点启动时会自动迁移历史 `.go` 密文 |
| 多语言必须 cn + en | 只写中文 → 英文界面下显示中文 | 语言相关键（Name/Description/Solution/AffectedProducts）补齐 `.en` 后缀 |
| `@meta` 大小写与空格 | 键写错、`@meta:` 写成别的形式 → 元数据为空、漏洞名为空 | 键名精确大小写；注释必须在行首；`=` 两侧空白可自动去除 |
| `scan.Sleep` 占用执行时间 | 长休眠导致整体超时 | 轮询场景控制次数与单次时长；总预算不要逼近 Timeout |
| POC 里写无界循环 | 超时后 goroutine 滞留后台，累积泄漏 | 所有循环必须有明确上界；避免 `for {}` |
| 缺 `package` 声明 | 加载时被静默跳过（不报错，只是模板"消失"） | 必须有 `package main` |
| 手工设置同名 Cookie | 显式头覆盖会话中同名 Cookie | 需要"先登录"时不要手写同名 Cookie 头 |
| 通用模板里硬编码绝对 URL | 别的任务拿着它去请求写死的主机（越界） | 用 `scan.Flow` 取当前流量；AI 生成链路会直接拒绝硬编码目标 |
| 响应体疑似被截断 | 单次读取上限 1MB | 大响应只取前段做判定；需要全量时改用多次分段请求 |
| `scan.Headers` key 大小写 | `scan.Flow.Headers["User-Agent"]` 取不到值 | 请求头/响应头 key 均为小写，写 `["user-agent"]` |
| `scan.TrimCut` 的第二参数 | 以为是"去子串"，实际是"字符集" | `TrimCut(s, cutset)` 去掉首尾任意 `cutset` 中的字符；按子串用 `Replace` |
| `RunPocTest` 的 go 分支不带代理 | 只能经代理访问的目标直连失败 | 走 GUI「Go模板」运行测试（`RunGoPocTest`）或扫描任务，它们会注入代理 |

> [!TIP]
> 写完模板先自查三件事：首行是不是 `//go:build ignore`？`@meta:Name` 是不是行首？`import "scan"` 用到的符号是不是都在 5.53 符号全表里？这三点能挡掉绝大多数"模板不生效"的问题。

<!-- en -->
# Scanner Node · Hot-loadable Go POC Development

> 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).

> [!TIP]
> 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.**

[[toc]]

---

## 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:

| Location | Directory and file name | Content | Notes |
|---|---|---|---|
| Controller | `poc/go/<VulnIde>.go` | Plaintext Go source | Scanned and loaded when the controller starts; the GUI editor and `GetGoPocDetail` both read it |
| Scanner node | `scan-poc/go/<VulnIde>.gopoc` | **AES-GCM ciphertext** | After the controller delivers it to the node, the node encrypts it with the global key and saves it into its own run directory |

> [!NOTE]
> 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

> [!IMPORTANT]
> **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.

| Dimension | YAML template | Hot-loadable Go | WASM template |
|---|---|---|---|
| Execution | Native Go engine, unmarshalled only once | Every run goes through the interpreter (compilation + reflection) | wazero runtime |
| Performance | Fastest | **An order of magnitude slower than YAML** | Moderate (compiled once, reusable) |
| Expressiveness | Declarative: request sequences + matching + regex + expressions + timing + reverse connection | **Turing-complete**; arbitrary loops, parsing and algorithms | Turing-complete, cross-language |
| Distribution | Single file | Single file (plaintext on the controller / ciphertext on the node) | Binary + signature |
| Maintenance cost | Low | High | High |
| Barrier to entry | None | Basic Go knowledge; no toolchain needed | Build chain + signature |
| Signature control | None (fingerprint sync) | None (fingerprint sync) | **Hard gate: unsigned templates never run** |
| Recommendation | **First choice** | Only for complex algorithms or custom parsing | For 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
//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):

```go
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:

| Form | Meaning |
|---|---|
| `// @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 Detection` | **Language suffix**; recognized for language-related keys only |
| `// @meta:DefaultLanguage=cn` | Base language; defaults to `cn` |

> [!WARNING]
> 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):

| Key | Type | Required | Default | Example value | Consequence of a mistake |
|---|---|---|---|---|---|
| `Name` | string | Yes (or `VulnName`) | empty | `SQL 注入检测` | The vulnerability name is empty and a blank entry appears in the list |
| `VulnName` | string | No | empty | `SQL 注入检测` | Alias of `Name` only; if both are missing the name is empty |
| `CVEId` | string | No | empty | `CVE-2024-0001` | The card shows no CVE number |
| `CweId` | string | No | empty | `CWE-89` | The card shows no CWE classification |
| `CvssScore` | string | No | empty | `9.8` | The card shows no CVSS score |
| `CvssVector` | string | No | empty | `CVSS:3.1/AV:N/...` | The card shows no vector string |
| `Level` | string | No | empty | `high` | An empty or misspelled value is reported as low severity |
| `Description` | string (language-related) | No | empty | `检测 SQL 报错特征` | The card's "Details" is empty |
| `Solution` | string (language-related) | No | empty | `使用参数化查询` | The card's "Remediation" is empty |
| `Author` | string | No | empty | `admin` | The author field is empty |
| `References` | string | No | empty | `https://example.com/advisory` | The reference links are empty (use `+=` for multiple lines) |
| `Fingerprint` | string | No | empty | `example-product` | The fingerprint is empty |
| `AffectedProducts` | string (language-related) | No | empty | `Example Product 1.0` | The affected products are empty |
| `Verification` | string | No | empty | `手动复现步骤` | The verification notes are empty |
| `TestDepth` | string | No | `pack` | `pack`/`single-dir`/`single-domain` | An empty value falls back to `pack` |
| `Confidence` | int | No | `80` | `90` | An invalid value, or one ≤ 0, uses 80 |
| `Enabled` | bool | No | `true` | `false` | The template is disabled when the value is `false` (case-insensitive) |
| `DefaultLanguage` | string | No | `cn` | `cn` | An 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
//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

> [!CAUTION]
> 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).

```go
// @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
//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),
		})
	}
}
```

> [!TIP]
> 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

| Line | Purpose | Consequence of a mistake |
|---|---|---|
| `//go:build ignore` | Makes `go build ./...` skip the file | A 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 name | The English UI shows Chinese |
| `// @meta:DefaultLanguage=cn` | Base language | Falls back to `cn` |
| `package main` | Marks a valid Go source file | Silently skipped at load time (the template "disappears") |
| `import "scan"` | Gets all injected symbols | Without the import, every `scan.` reference is undefined |
| `func main()` | **The only entry point**, executed automatically by the interpreter | Without `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:

```go
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.

> [!WARNING]
> **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

```text
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

```text
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

> [!NOTE]
> **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**.

> [!WARNING]
> 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):

| Parameter | Default | Notes |
|---|---|---|
| Single-run timeout | 30s | Also used as the HTTP client timeout; a timeout marks the execution as failed |
| Proxy | empty (direct) | HTTP proxy address, delivered at task level |
| Certificate verification | certificate errors are ignored for both debugging and tasks | Makes it easy to test self-signed lab targets |
| Redirects | not followed by default | Enabled by the debug/task side when needed |
| Response body limit | 1MB | Maximum 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

> [!NOTE]
> 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**:

| Field | Type | Meaning | Who fills it, in what format |
|---|---|---|---|
| `URL` | `string` | Full request URL | Copied from the original URL in the packet |
| `Method` | `string` | Request method | Copied from the packet (such as `GET`/`POST`) |
| `Host` | `string` | Target host name | Prefers the domain in the packet; when empty, the host name (without port) is parsed from the URL |
| `Scheme` | `string` | Scheme | Starts from the packet's TLS flag, then is overridden by the scheme parsed from the URL; the result is `http`/`https` |
| `Path` | `string` | Request path | Filled from the path part of the URL |
| `Query` | `string` | Query parameters | Filled from the URL's raw query string (**without `?`**) |
| `Headers` | `map[string]string` | Request headers | Parsed from the raw request header text, with **keys already lowercased**; access as `scan.Flow.Headers["user-agent"]` |
| `Body` | `string` | Request body | Copied from the packet's request body and converted to a string |
| `RawHeaders` | `string` | Raw request header text (multiple lines) | Copied from the packet's raw request header text |

**Code snippet**:

```go
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**:

| Key | When it appears | Meaning | Example value |
|---|---|---|---|
| `timeout` | Scan task, GUI debugging | Execution timeout (seconds) | `"30"` |
| `proxy` | Scan task, GUI debugging | Proxy address | `"http://127.0.0.1:8080"` |
| `seed` | Scan task | Scan speed/depth seed | `"1"` |
| `taskIde` | Scan task | Task identifier | `"task-..."` |
| `target` | Unified test verification (RunPocTest) | Target URL | `"https://target/"` |

**Code snippet**:

```go
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**:

| Field | Type | Meaning | How to use |
|---|---|---|---|
| `Name` | `string` | Vulnerability name | Empty uses the template's `@meta:Name`; when set it overrides the card title and clears the template's multilingual snapshot |
| `Detail` | `string` | Vulnerability details | Empty uses the template description; when set it overrides the card's "Details" |
| `Evidence` | `string` | Hit evidence (response fragment / message text) | Displayed as the "response" when `Request`/`Response` are not set |
| `Level` | `string` | Severity `critical`/`high`/`medium`/`low` | Mapped to 4/3/2/1; empty or unrecognized falls back to the template level, and if that is also empty, low severity |
| `URL` | `string` | Hit URL | Empty uses the current `Flow.URL` |
| `CVEId` | `string` | CVE number | Empty uses the template's `@meta:CVEId`; when set it overrides |
| `Request` | `string` | Triggering request message (text) | Displayed together with `Response` as a request-response pair |
| `Response` | `string` | Hit response message (text) | Same as above |
| `SensitiveText` | `string` | Raw fragment of the sensitive information (200 characters before and after the hit) | Used only by sensitive-information templates; highlighted in the GUI |
| `SensitiveKeywords` | `[]string` | List of keywords actually matched | The GUI plain-text viewer highlights based on it |
| `SensitiveMatchRule` | `string` | Formula/rule actually matched | Sensitive-information templates record the matching basis here |

**Code snippet**:

```go
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**:

| Field | Type | Meaning | How to use |
|---|---|---|---|
| `StatusCode` | `int` | HTTP status code | **0 means the request failed** (read `Error` in that case); otherwise 200/302/404 and so on |
| `Body` | `string` | Response body text | Trimmed by the 1MB limit (`MaxBodySize`) |
| `Headers` | `map[string]string` | Response headers, lowercase keys | `resp.Headers["content-type"]` |
| `Cookies` | `[]string` | Raw list of response `Set-Cookie` values | `len(resp.Cookies) > 0` tells whether a session was issued |
| `URL` | `string` | Final request URL (including after redirects) | `resp.URL` is more accurate when reporting |
| `TimeMs` | `int64` | Request duration (milliseconds) | For time-based blind detection |
| `RawHeaders` | `string` | Raw response header text (multiple lines) | Concatenate with `Body` to display the response message |
| `Error` | `string` | Error message when the request fails (empty on success) | **Check `Error` before checking `StatusCode`** |

**Code snippet**:

```go
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**:

```go
scan.Log(msg string)
```

**Parameter table**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `msg` | `string` | Any debug text | Constants, `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**:

```go
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**:

```go
scan.Report(v VulnFinding)
```

**Parameter table**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `v` | `scan.VulnFinding` | One finding | Build 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**:

```go
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):

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

**Parameter table**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `method` | `string` | HTTP method; an empty string is treated as `GET` | A constant or `scan.Flow.Method` | `"POST"` |
| `reqURL` | `string` | Full URL | `scan.Flow.URL`, or assembled from `scan.URLHost/URLScheme` | `"https://t/api/login"` |
| `headers` | `map[string]string` | Custom request headers; nil adds none | A map literal | `map[string]string{"Content-Type":"application/json"}` |
| `body` | `string` | Request body; an empty string means no body | A 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)**:

```go
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**:

```go
scan.HTTPGet(rawurl string) scan.HTTPResponse
```

**Parameter table**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `rawurl` | `string` | Full URL | `scan.Flow.URL`, or an assembled target | `"https://target/"` |

**Returns**: `scan.HTTPResponse` (fields in 5.4).

**Code snippet**:

```go
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**:

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

**Parameter table**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `rawurl` | `string` | Full URL | An assembled target | `"https://target/api"` |
| `body` | `string` | Request body | A form string or arbitrary text | `"user=admin&pass=admin"` |

**Returns**: `scan.HTTPResponse` (fields in 5.4).

**Code snippet**:

```go
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**:

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

**Parameter table**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `rawurl` | `string` | Full URL | An assembled target | `"https://target/api/login"` |
| `jsonBody` | `string` | JSON text | Hand-written, or `scan.JSONDump(object)` | `{"user":"admin"}` |

**Returns**: `scan.HTTPResponse` (fields in 5.4).

**Code snippet**:

```go
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**:

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

**Parameter table**: identical to `scan.HTTP`.

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `method` | `string` | HTTP method; empty means `GET` | A constant | `"PUT"` |
| `reqURL` | `string` | Full URL | `scan.Flow.URL` | `"https://t/api"` |
| `headers` | `map[string]string` | Custom request headers | A map literal | `map[string]string{"Authorization":"Bearer x"}` |
| `body` | `string` | Request body; an empty string means none | A constant | `""` |

**Returns**: `scan.HTTPResponse` (fields in 5.4).

**Code snippet**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `s` | `string` | Any string | `resp.Body`, etc. | `"Root:X:0:0"` |

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

**Code snippet**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `s` | `string` | Any string | `resp.Body` | `"abc"` |

**Returns**: `string`.

**Code snippet**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `s` | `string` | Any string | Response text | `"  admin\n"` |

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

**Code snippet**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `s` | `string` | String to trim | Response text | `"///admin///"` |
| `cutset` | `string` | Set of characters to remove | A constant | `"/"` |

**Returns**: `string`.

**Code snippet**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `s` | `string` | Original string | Response text | `"a-b-c"` |
| `old` | `string` | Text to be replaced | A constant | `"-"` |
| `new` | `string` | Replacement text | A constant | `"_"` |

**Returns**: `string`.

**Code snippet**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `s` | `string` | Original string | Response text | `"a,b,c"` |
| `sep` | `string` | Separator | A constant | `","` |

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

**Code snippet**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `elems` | `[]string` | String slice | The result of `scan.Split`, `scan.RegexExtractAll`, etc. | `[]string{"a","b"}` |
| `sep` | `string` | Separator | A constant | `","` |

**Returns**: `string`.

**Code snippet**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `s` | `string` | String to search | `resp.Body` | `"hello world"` |
| `substr` | `string` | Substring to find | A constant | `"world"` |

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

**Code snippet**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `s` | `string` | String to check | `resp.Body` | `"http://x"` |
| `prefix` | `string` | Prefix | A constant | `"http"` |

**Returns**: `bool`.

**Code snippet**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `s` | `string` | String to check | A path | `"/admin/"` |
| `suffix` | `string` | Suffix | A constant | `"/"` |

**Returns**: `bool`.

**Code snippet**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `s` | `string` | String to search | `resp.Body` | `"abcabc"` |
| `substr` | `string` | Substring | A constant | `"bc"` |

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

**Code snippet**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `s` | `string` | Original string | `resp.Body` | `"abcdef"` |
| `start` | `int` | Start index (inclusive) | A constant | `0` |
| `end` | `int` | End index (exclusive) | A constant | `3` |

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

**Code snippet**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `s` | `string` | Any string | `resp.Body` | `"hello"` |

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

**Code snippet**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `s` | `string` | Original string | A constant | `"A"` |
| `count` | `int` | Repetitions | A constant | `5` |

**Returns**: `string`.

**Code snippet**:

```go
// 构造超长参数触发异常
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `s` | `string` | Plain text | Credentials, a payload | `"admin:admin"` |

**Returns**: `string` (Base64 text).

**Code snippet**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `s` | `string` | Base64 text | An encoded field in the response | `"YWRtaW4="` |

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

**Code snippet**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `s` | `string` | Plain text | A parameter value | `"a b&c"` |

**Returns**: `string`.

**Code snippet**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `s` | `string` | Escaped string | A response or URL | `"a%20b"` |

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

**Code snippet**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `s` | `string` | Plain text | Anything | `"abc"` |

**Returns**: `string` (lowercase hexadecimal).

**Code snippet**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `s` | `string` | Hexadecimal text | A response field | `"616263"` |

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

**Code snippet**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `s` | `string` | Plain text | Anything | `"admin"` |

**Returns**: `string` (32 lowercase hexadecimal characters).

**Code snippet**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `s` | `string` | Plain text | Anything | `"abc"` |

**Returns**: `string` (40 lowercase hexadecimal characters).

**Code snippet**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `s` | `string` | Plain text | Anything | `"abc"` |

**Returns**: `string` (64 lowercase hexadecimal characters).

**Code snippet**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `s` | `string` | Text to search | `resp.Body` | `"root:x:0:0"` |
| `pattern` | `string` | Regular expression | A 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**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `s` | `string` | Text to search | `resp.Body` | `"user: admin"` |
| `pattern` | `string` | Regex (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**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `s` | `string` | Text to search | `resp.Body` | `"a1 b2 c3"` |
| `pattern` | `string` | Regex | A constant | `"\d+"` |
| `sep` | `string` | Join separator | A constant | `","` |

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

**Code snippet**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `jsonStr` | `string` | JSON text | `resp.Body` | `{"data":{"role":"admin"}}` |
| `path` | `string` | Dot path supporting `a.b.c` and `items.0.name` | A 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**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `s` | `string` | Text to check | `resp.Body` | `{"ok":true}` |

**Returns**: `bool`.

**Code snippet**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `v` | `interface{}` | Any value | A map/slice/number/boolean | `map[string]int{"code":200}` |

**Returns**: `string`; **serialization failure returns an empty string**.

**Code snippet**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `min` | `int` | Lower bound (inclusive) | A constant | `1` |
| `max` | `int` | Upper bound (exclusive) | A constant | `100` |

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

**Code snippet**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `n` | `int` | Desired length | A constant | `12` |

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

**Code snippet**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `n` | `int` | Desired length | A constant | `6` |

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

**Code snippet**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `n` | `int` | Desired length | A constant | `6` |

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

**Code snippet**:

```go
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**:

```go
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**:

```go
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**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `ms` | `int` | Milliseconds to sleep | A constant | `500` |

**Returns**: nothing.

**Code snippet**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `rawurl` | `string` | Full URL | `scan.Flow.URL` | `"https://a.com:8443/x"` |

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

**Code snippet**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `rawurl` | `string` | Full URL | `scan.Flow.URL` | `"https://a.com/a/b?x=1"` |

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

**Code snippet**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `rawurl` | `string` | Full URL | `scan.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**:

```go
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**:

| Parameter | Type | What to pass | Where it comes from | Example value |
|---|---|---|---|---|
| `rawurl` | `string` | Full URL | `scan.Flow.URL` | `"https://a.com/"` |

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

**Code snippet**:

```go
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)

> [!IMPORTANT]
> 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.

| Category | All symbols |
|---|---|
| Data | `scan.Flow`、`scan.Config` |
| Types | `scan.VulnFinding`、`scan.HTTPResponse` |
| Core | `scan.Log`、`scan.Report`、`scan.HTTP` |
| Convenience HTTP | `scan.HTTPGet`、`scan.HTTPPost`、`scan.HTTPJSONPost`、`scan.HTTPHeader` |
| Strings | `scan.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/Hashing | `scan.Base64Encode`、`scan.Base64Decode`、`scan.URLEncode`、`scan.URLDecode`、`scan.HexEncode`、`scan.HexDecode`、`scan.MD5`、`scan.SHA1`、`scan.SHA256` |
| Regex | `scan.RegexMatch`、`scan.RegexExtract`、`scan.RegexExtractAll` |
| JSON | `scan.JSONGet`、`scan.JSONValid`、`scan.JSONDump` |
| Random/Time | `scan.RandInt`、`scan.RandString`、`scan.RandAlpha`、`scan.RandNum`、`scan.UUID`、`scan.Timestamp`、`scan.TimestampMs`、`scan.Sleep` |
| URL parsing | `scan.URLHost`、`scan.URLPath`、`scan.URLQuery`、`scan.URLScheme` |

---

## 6. Cookie Session Semantics

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.

```go
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.

> [!TIP]
> 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` field | Effect on the card |
|---|---|
| `Name` | Overrides 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) |
| `Detail` | Overrides the template description as the card's "Details"; it likewise clears the multilingual snapshot |
| `Evidence` | When `Request`/`Response` are not set, it is displayed as the "response" in the request-response list |
| `Level` | Mapped 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` |
| `URL` | Hit URL; empty uses the current `Flow.URL` |
| `CVEId` | Overrides the template's CVE number |
| `Request` / `Response` | Displayed as a request-response message pair |
| `SensitiveText` / `SensitiveKeywords` / `SensitiveMatchRule` | The three sensitive-information highlighting fields, used only by sensitive-information templates |

> [!WARNING]
> 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.

### 8.2 Related Commands and Data Structures

| Command | Direction | Purpose |
|---|---|---|
| `RunGoPocTest` | GUI → controller → scanner node | Debug-run a hot-loadable Go POC; returns `GoPocTestResult` (findings + logs + HTTP count) |
| `GetGoPocDetail` | GUI → controller | Fetch the full details of a vulnerability (including `GoSource`) so editing uses the latest source |
| `PocTestRequest` / `RunPocTest` | GUI → controller → scanner node | Unified 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.

> [!NOTE]
> 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 surface | What to look at | Data source | When to use |
|---|---|---|---|
| GUI "Go template" debug panel | `scan.Log` output + matched findings + HTTP request count | `GoPocTestResult`'s `Logs` / `Findings` / `HttpCount` | Verifying logic line by line during development; the fastest route |
| Task-result vulnerability card | Vulnerability name / details / severity / request-response messages | The vulnerability detail reported by the node (queryable once stored) | Verifying what a real scan task produces |
| Controller / node logs | POC execution failures, timeouts and per-run logs | Node / controller logs | Troubleshooting task-time issues and checking whether the POC was scheduled |
| Unified test verification result | `PocTestResult`'s `Success` / `Found` / `Findings` | The go branch of `RunPocTest` | AI 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).

> [!TIP]
> 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

| Symptom | Cause | Fix |
|---|---|---|
| `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 source | The 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 template | Every run goes through the interpreter (compilation + reflection), which is **an order of magnitude slower** | Always 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 imported | The 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 `Sleep` | Limit 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 low | `Report`'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 error | Node-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/descriptions | Only `cn` was written, with no `.en` added | Add 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 used | Spell key names exactly; comments must start the line; whitespace around `=` is trimmed automatically |
| The run as a whole times out with logs stuck in polling | `scan.Sleep` consumes the execution time budget | Limit each sleep and the iteration count; leave enough headroom in the total polling time |
| The node process's memory/goroutines keep growing after a task | The POC contains an unbounded loop, and the interpreter **cannot force cancellation** after a timeout, so the goroutine lingers in the background | Every loop must have a clear upper bound; avoid `for {}` |
| The template "disappears" from the list and nothing happens | The `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 detected | Besides `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 directly | The 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 matched | The single-read limit is 1MB; anything beyond is truncated | Inspect 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 value | Do 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 task | The code hard-codes an absolute URL | Take the current flow from `scan.Flow`; the AI generation path rejects hard-coded targets outright |

> [!TIP]
> 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
//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),
		})
	}
}
```

### 10.2 Example 2: Two-Step Cookie Session (Log In First, Then Access a Protected Endpoint)

```go
//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
//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

| Pitfall | Symptom | Avoidance |
|---|---|---|
| Forgetting `//go:build ignore` | Templates in the source tree are compiled by `go build ./...`, causing errors or being packaged | The first line must be `//go:build ignore`, followed by a blank line; platform-generated skeletons already include it |
| Expecting YAML-level performance | Every run goes through the interpreter (compilation + reflection), which is **an order of magnitude slower** | Always use YAML when possible; reserve Go for complex algorithms |
| Importing an unregistered package | `import` of a third-party package → interpretation failure | The platform registers only the Go standard library and `scan`; extend through the `scan` package instead of third-party imports |
| The 30s timeout | Execution over 30s returns a timeout error; moreover the interpreter **cannot truly cancel** the goroutine | Limit the number of requests and the amount of logic; `scan.Sleep` consumes the execution time budget |
| Using a symbol that does not exist in `scan` | Interpretation 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 `Report` | Empty or misspelled → falls back to the template level, or low severity if the template has none | Always use the four lowercase values `critical`/`high`/`medium`/`low` |
| The node-side extension must be `.gopoc` | Ciphertext 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 + en | Only Chinese written → the English UI shows Chinese | Add the `.en` suffix for language-related keys (Name/Description/Solution/AffectedProducts) |
| `@meta` case and spaces | A misspelled key or a wrong form of `@meta:` → empty metadata and an empty vulnerability name | Spell key names exactly; comments must start the line; whitespace around `=` is trimmed automatically |
| `scan.Sleep` consumes execution time | A long sleep causes an overall timeout | In polling scenarios limit the count and each duration; keep the total budget away from the Timeout |
| Writing an unbounded loop in a POC | After a timeout the goroutine lingers in the background, accumulating a leak | Every loop must have a clear upper bound; avoid `for {}` |
| Missing the `package` declaration | Silently skipped at load time (no error; the template simply "disappears") | `package main` is required |
| Setting a same-named Cookie by hand | The explicit header overrides the same-named Cookie in the session | When you need "log in first", do not hand-write a same-named Cookie header |
| Hard-coding an absolute URL in a generic template | Other 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 truncated | The single-read limit is 1MB | For large responses, detect on the front part only; for full content, switch to several ranged requests |
| `scan.Headers` key case | `scan.Flow.Headers["User-Agent"]` retrieves nothing | Request/response header keys are all lowercase; write `["user-agent"]` |
| The second parameter of `scan.TrimCut` | Assumed 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 proxy | A target reachable only through a proxy fails to connect directly | Use the GUI's "Go template" run test (`RunGoPocTest`) or a scan task; they inject the proxy |

> [!TIP]
> 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.
