feat: Web 版独立入口 + SQLite 持久化存储

- 拆分 main.go 为 main_cli.go 和 main_web.go,Web 版不再包含 CLI 参数解析
- Web 版直接启动 HTTP 服务,通过 -port/-lang 控制,无需 -web flag
- 结果存储从内存 map 替换为 SQLite(modernc.org/sqlite,纯 Go 零 CGO)
- 数据库文件 ~/.fscan/results.db,进程重启后结果不丢失
- 修复结果分布面板跟随 tab 筛选联动的问题
This commit is contained in:
ZacharyZcR
2026-06-14 22:23:44 +08:00
parent d0295dcb92
commit e0468ecd35
12 changed files with 285 additions and 131 deletions
+1 -1
View File
@@ -64,7 +64,7 @@ build-web: build-ui
@echo "$(BLUE)构建Web版本...$(NC)" @echo "$(BLUE)构建Web版本...$(NC)"
$(GO) build -tags web -ldflags="-s -w" -trimpath -o $(BINARY_NAME)-web . $(GO) build -tags web -ldflags="-s -w" -trimpath -o $(BINARY_NAME)-web .
@echo "$(GREEN)✓ 构建完成: $(BINARY_NAME)-web$(NC)" @echo "$(GREEN)✓ 构建完成: $(BINARY_NAME)-web$(NC)"
@echo "$(BLUE)提示: 运行 ./$(BINARY_NAME)-web -web 启动Web界面$(NC)" @echo "$(BLUE)提示: 运行 ./$(BINARY_NAME)-web 启动Web界面(默认端口 10240$(NC)"
## build-ui: 构建前端(需要Node.js和npm ## build-ui: 构建前端(需要Node.js和npm
build-ui: build-ui:
+4 -15
View File
@@ -2,19 +2,8 @@
package common package common
import ( // WebMode Web版本始终为true
"flag" const WebMode = true
"github.com/shadow1ng/fscan/common/i18n" // WebPort 不再使用,端口由 main_web.go 的 -port 参数控制
) var WebPort = 0
// WebMode 表示是否启动Web管理界面
var WebMode bool
// WebPort Web服务器端口
var WebPort int
func init() {
flag.BoolVar(&WebMode, "web", false, i18n.GetText("flag_web_mode"))
flag.IntVar(&WebPort, "webport", 10240, i18n.GetText("flag_web_port"))
}
+10 -3
View File
@@ -1,6 +1,6 @@
module github.com/shadow1ng/fscan module github.com/shadow1ng/fscan
go 1.20 go 1.25.0
require ( require (
github.com/fatih/color v1.18.0 github.com/fatih/color v1.18.0
@@ -24,13 +24,14 @@ require (
go.ciq.dev/go-rsync v0.0.0-20240304021629-0a3bb196e6d1 go.ciq.dev/go-rsync v0.0.0-20240304021629-0a3bb196e6d1
golang.org/x/crypto v0.31.0 golang.org/x/crypto v0.31.0
golang.org/x/net v0.32.0 golang.org/x/net v0.32.0
golang.org/x/sys v0.28.0 golang.org/x/sys v0.42.0
golang.org/x/term v0.27.0 golang.org/x/term v0.27.0
golang.org/x/text v0.21.0 golang.org/x/text v0.21.0
google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c
google.golang.org/protobuf v1.28.1 google.golang.org/protobuf v1.28.1
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.52.0
) )
require ( require (
@@ -38,6 +39,7 @@ require (
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/geoffgarside/ber v1.1.0 // indirect github.com/geoffgarside/ber v1.1.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect
@@ -53,9 +55,14 @@ require (
github.com/kr/pretty v0.3.0 // indirect github.com/kr/pretty v0.3.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/stoewer/go-strcase v1.2.0 // indirect github.com/stoewer/go-strcase v1.2.0 // indirect
golang.org/x/sync v0.11.0 // indirect golang.org/x/sync v0.20.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
modernc.org/libc v1.72.3 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
) )
+48 -3
View File
@@ -5,6 +5,7 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 h1:yL7+Jz0jTC6yykIK/Wh74gnTJnrGr5AyrNMXuA0gves= github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 h1:yL7+Jz0jTC6yykIK/Wh74gnTJnrGr5AyrNMXuA0gves=
@@ -16,6 +17,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
@@ -50,6 +53,8 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
@@ -65,6 +70,8 @@ github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hirochachacha/go-smb2 v1.1.0 h1:b6hs9qKIql9eVXAiN0M2wSFY5xnhbHAQoCwRKbaRTZI= github.com/hirochachacha/go-smb2 v1.1.0 h1:b6hs9qKIql9eVXAiN0M2wSFY5xnhbHAQoCwRKbaRTZI=
github.com/hirochachacha/go-smb2 v1.1.0/go.mod h1:8F1A4d5EZzrGu5R7PU163UcMRDJQl4FtcxjBfsY8TZE= github.com/hirochachacha/go-smb2 v1.1.0/go.mod h1:8F1A4d5EZzrGu5R7PU163UcMRDJQl4FtcxjBfsY8TZE=
github.com/huin/asn1ber v0.0.0-20120622192748-af09f62e6358 h1:hVXNJ57IHkOA8FBq80UG263MEBwNUMfS9c82J2QE5UQ= github.com/huin/asn1ber v0.0.0-20120622192748-af09f62e6358 h1:hVXNJ57IHkOA8FBq80UG263MEBwNUMfS9c82J2QE5UQ=
@@ -108,6 +115,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/go-vnc v0.0.0-20150629162542-723ed9867aed h1:FI2NIv6fpef6BQl2u3IZX/Cj20tfypRF4yd+uaHOMtI= github.com/mitchellh/go-vnc v0.0.0-20150629162542-723ed9867aed h1:FI2NIv6fpef6BQl2u3IZX/Cj20tfypRF4yd+uaHOMtI=
github.com/mitchellh/go-vnc v0.0.0-20150629162542-723ed9867aed/go.mod h1:3rdaFaCv4AyBgu5ALFM0+tSuHrBh6v692nyQe3ikrq0= github.com/mitchellh/go-vnc v0.0.0-20150629162542-723ed9867aed/go.mod h1:3rdaFaCv4AyBgu5ALFM0+tSuHrBh6v692nyQe3ikrq0=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM= github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM=
github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4= github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4=
github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg= github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg=
@@ -117,6 +126,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
@@ -133,6 +144,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
@@ -159,6 +171,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -187,8 +201,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -204,8 +218,9 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -236,6 +251,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
@@ -270,3 +287,31 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ=
modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU=
modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.52.0 h1:p4dhYh2tXZCiyaqHwRVJDjIGKWyXayiQpThxgDzJaxo=
modernc.org/sqlite v1.52.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
+2 -10
View File
@@ -1,3 +1,5 @@
//go:build !web
package main package main
import ( import (
@@ -10,7 +12,6 @@ import (
"github.com/shadow1ng/fscan/common/debug" "github.com/shadow1ng/fscan/common/debug"
"github.com/shadow1ng/fscan/common/i18n" "github.com/shadow1ng/fscan/common/i18n"
"github.com/shadow1ng/fscan/core" "github.com/shadow1ng/fscan/core"
"github.com/shadow1ng/fscan/web"
// 导入统一插件系统 // 导入统一插件系统
_ "github.com/shadow1ng/fscan/plugins/local" _ "github.com/shadow1ng/fscan/plugins/local"
@@ -33,15 +34,6 @@ func main() {
os.Exit(1) os.Exit(1)
} }
// Web模式:启动Web服务器
if common.WebMode {
if err := web.StartServer(common.WebPort); err != nil {
common.LogError(err.Error())
os.Exit(1)
}
return
}
// 检查参数互斥性 // 检查参数互斥性
if err := common.ValidateExclusiveParams(&info); err != nil { if err := common.ValidateExclusiveParams(&info); err != nil {
common.LogError(i18n.Tr("error_generic", err)) common.LogError(i18n.Tr("error_generic", err))
+33
View File
@@ -0,0 +1,33 @@
//go:build web
package main
import (
"flag"
"fmt"
"os"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/i18n"
"github.com/shadow1ng/fscan/web"
// 导入统一插件系统
_ "github.com/shadow1ng/fscan/plugins/local"
_ "github.com/shadow1ng/fscan/plugins/services"
_ "github.com/shadow1ng/fscan/plugins/web"
)
func main() {
port := flag.Int("port", 10240, "Web server listen port")
lang := flag.String("lang", "zh", "Language (zh/en)")
flag.Parse()
i18n.SetLanguage(*lang)
fmt.Printf("fscan web v%s\n", common.GetVersion())
if err := web.StartServer(*port); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
+24 -5
View File
@@ -21,6 +21,7 @@
"@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "^1.2.8",
"@rollup/rollup-linux-x64-gnu": "^4.61.1",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"i18next": "^25.7.3", "i18next": "^25.7.3",
@@ -2496,15 +2497,16 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-gnu": { "node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.54.0", "version": "4.61.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz",
"integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true,
"os": [ "os": [
"linux" "linux"
] ]
@@ -5318,6 +5320,23 @@
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
"node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz",
"integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/run-parallel": { "node_modules/run-parallel": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+1
View File
@@ -23,6 +23,7 @@
"@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "^1.2.8",
"@rollup/rollup-linux-x64-gnu": "^4.61.1",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"i18next": "^25.7.3", "i18next": "^25.7.3",
+2 -2
View File
@@ -53,14 +53,14 @@ export function ResultsPage() {
const fetchResults = useCallback(async () => { const fetchResults = useCallback(async () => {
setLoading(true); setLoading(true);
try { try {
const data = await getResults(filter === 'all' ? undefined : filter); const data = await getResults();
setResults(data.items); setResults(data.items);
} catch (err) { } catch (err) {
console.error('Failed to fetch results:', err); console.error('Failed to fetch results:', err);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [filter]); }, []);
useEffect(() => { useEffect(() => {
fetchResults(); fetchResults();
+146 -78
View File
@@ -3,15 +3,19 @@
package api package api
import ( import (
"database/sql"
"encoding/csv" "encoding/csv"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"os"
"path/filepath"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/shadow1ng/fscan/common/i18n" "github.com/shadow1ng/fscan/common/i18n"
_ "modernc.org/sqlite"
) )
// ResultItem 扫描结果项 // ResultItem 扫描结果项
@@ -24,26 +28,66 @@ type ResultItem struct {
Details interface{} `json:"details,omitempty"` Details interface{} `json:"details,omitempty"`
} }
// ResultStore 结果存储 // ResultStore SQLite 结果存储
type ResultStore struct { type ResultStore struct {
mu sync.RWMutex mu sync.RWMutex
items []ResultItem db *sql.DB
counter int64
stats ScanStats
// 去重
seen map[string]bool
// service 类型按 target 索引,用于更新
serviceIndex map[string]int
} }
// 全局结果存储 // 全局结果存储
var globalResultStore = &ResultStore{ var globalResultStore *ResultStore
items: make([]ResultItem, 0),
seen: make(map[string]bool), func init() {
serviceIndex: make(map[string]int), store, err := NewResultStore()
if err != nil {
panic(fmt.Sprintf("failed to init result store: %v", err))
}
globalResultStore = store
} }
// Add 添加结果,返回格式化后的结果项(去重,重复则返回nil) // dbPath 返回数据库文件路径
func dbPath() string {
home, err := os.UserHomeDir()
if err != nil {
home = "."
}
dir := filepath.Join(home, ".fscan")
_ = os.MkdirAll(dir, 0755)
return filepath.Join(dir, "results.db")
}
// NewResultStore 创建 SQLite 存储
func NewResultStore() (*ResultStore, error) {
db, err := sql.Open("sqlite", dbPath())
if err != nil {
return nil, err
}
// WAL 模式,提升并发读写
if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil {
db.Close()
return nil, err
}
if _, err := db.Exec(`
CREATE TABLE IF NOT EXISTS results (
id INTEGER PRIMARY KEY AUTOINCREMENT,
time TEXT NOT NULL,
type TEXT NOT NULL,
target TEXT NOT NULL,
status TEXT NOT NULL DEFAULT '',
details TEXT NOT NULL DEFAULT '{}',
UNIQUE(type, target, status)
)
`); err != nil {
db.Close()
return nil, err
}
return &ResultStore{db: db}, nil
}
// Add 添加结果(去重,重复返回 nil)
func (s *ResultStore) Add(result interface{}) *ResultItem { func (s *ResultStore) Add(result interface{}) *ResultItem {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
@@ -53,10 +97,10 @@ func (s *ResultStore) Add(result interface{}) *ResultItem {
Details: result, Details: result,
} }
// 根据结果类型分类 // 解析结果字段
if m, ok := result.(map[string]interface{}); ok { if m, ok := result.(map[string]interface{}); ok {
if t, ok := m["type"].(string); ok { if t, ok := m["type"].(string); ok {
item.Type = strings.ToLower(t) // 统一转小写 item.Type = strings.ToLower(t)
} }
if target, ok := m["target"].(string); ok { if target, ok := m["target"].(string); ok {
item.Target = target item.Target = target
@@ -64,64 +108,58 @@ func (s *ResultStore) Add(result interface{}) *ResultItem {
if status, ok := m["status"].(string); ok { if status, ok := m["status"].(string); ok {
item.Status = status item.Status = status
} }
// 从details提取更多信息
if details, ok := m["details"].(map[string]interface{}); ok { if details, ok := m["details"].(map[string]interface{}); ok {
item.Details = details item.Details = details
// 组合 target:port
if port, ok := details["port"]; ok { if port, ok := details["port"]; ok {
if item.Target != "" && !strings.Contains(item.Target, ":") { if item.Target != "" && !strings.Contains(item.Target, ":") {
item.Target = fmt.Sprintf("%s:%v", item.Target, port) item.Target = fmt.Sprintf("%s:%v", item.Target, port)
} }
} }
// 构建更有意义的status
item.Status = buildStatusFromDetails(item.Type, item.Status, details) item.Status = buildStatusFromDetails(item.Type, item.Status, details)
} }
} }
// 生成去重键 detailsJSON, _ := json.Marshal(item.Details)
key := fmt.Sprintf("%s|%s|%s", item.Type, item.Target, item.Status)
if s.seen[key] {
return nil // 完全重复,不添加
}
// service/port 类型特殊处理:同一 target 只保留最详细的 // service/port 类型:同一 target 只保留最详细的
if item.Type == "service" || item.Type == "port" { if item.Type == "service" || item.Type == "port" {
indexKey := item.Type + "|" + item.Target var existID int64
if idx, exists := s.serviceIndex[indexKey]; exists { var existStatus string
oldStatus := s.items[idx].Status err := s.db.QueryRow(
// 如果旧的是基础状态,新的更详细,则更新 "SELECT id, status FROM results WHERE type = ? AND target = ? LIMIT 1",
if (oldStatus == "identified" || oldStatus == "open" || oldStatus == "") && item.Type, item.Target,
).Scan(&existID, &existStatus)
if err == nil {
// 已有记录,判断是否需要更新
if (existStatus == "identified" || existStatus == "open" || existStatus == "") &&
item.Status != "identified" && item.Status != "open" && item.Status != "" { item.Status != "identified" && item.Status != "open" && item.Status != "" {
s.items[idx].Status = item.Status s.db.Exec(
s.items[idx].Details = item.Details "UPDATE results SET status = ?, details = ?, time = ? WHERE id = ?",
s.items[idx].Time = item.Time item.Status, string(detailsJSON), item.Time.Format(time.RFC3339), existID,
s.seen[key] = true )
return &s.items[idx] item.ID = existID
return &item
} }
// 否则跳过(保留已有信息)
return nil return nil
} }
// 新记录,记录索引
s.serviceIndex[indexKey] = len(s.items)
} }
s.seen[key] = true // 插入(UNIQUE 约束自动去重)
res, err := s.db.Exec(
// 统计 "INSERT OR IGNORE INTO results (time, type, target, status, details) VALUES (?, ?, ?, ?, ?)",
switch item.Type { item.Time.Format(time.RFC3339), item.Type, item.Target, item.Status, string(detailsJSON),
case "host": )
s.stats.HostsScanned++ if err != nil {
case "port": return nil
s.stats.PortsScanned++
case "service":
s.stats.ServicesFound++
case "vuln":
s.stats.VulnsFound++
} }
s.counter++ affected, _ := res.RowsAffected()
item.ID = s.counter if affected == 0 {
s.items = append(s.items, item) return nil // 重复
}
item.ID, _ = res.LastInsertId()
return &item return &item
} }
@@ -129,25 +167,71 @@ func (s *ResultStore) Add(result interface{}) *ResultItem {
func (s *ResultStore) List() []ResultItem { func (s *ResultStore) List() []ResultItem {
s.mu.RLock() s.mu.RLock()
defer s.mu.RUnlock() defer s.mu.RUnlock()
return append([]ResultItem{}, s.items...)
rows, err := s.db.Query("SELECT id, time, type, target, status, details FROM results ORDER BY id")
if err != nil {
return nil
}
defer rows.Close()
return scanRows(rows)
} }
// Stats 获取统计信息 // Stats 获取统计信息
func (s *ResultStore) Stats() ScanStats { func (s *ResultStore) Stats() ScanStats {
s.mu.RLock() s.mu.RLock()
defer s.mu.RUnlock() defer s.mu.RUnlock()
return s.stats
var stats ScanStats
rows, err := s.db.Query("SELECT type, COUNT(*) FROM results GROUP BY type")
if err != nil {
return stats
}
defer rows.Close()
for rows.Next() {
var t string
var count int
if rows.Scan(&t, &count) == nil {
switch t {
case "host":
stats.HostsScanned = count
case "port":
stats.PortsScanned = count
case "service":
stats.ServicesFound = count
case "vuln":
stats.VulnsFound = count
}
}
}
return stats
} }
// Clear 清空结果 // Clear 清空结果
func (s *ResultStore) Clear() { func (s *ResultStore) Clear() {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
s.items = make([]ResultItem, 0) s.db.Exec("DELETE FROM results")
s.counter = 0 }
s.stats = ScanStats{}
s.seen = make(map[string]bool) // scanRows 解析查询结果
s.serviceIndex = make(map[string]int) func scanRows(rows *sql.Rows) []ResultItem {
var items []ResultItem
for rows.Next() {
var item ResultItem
var timeStr, detailsStr string
if err := rows.Scan(&item.ID, &timeStr, &item.Type, &item.Target, &item.Status, &detailsStr); err != nil {
continue
}
item.Time, _ = time.Parse(time.RFC3339, timeStr)
var details interface{}
if json.Unmarshal([]byte(detailsStr), &details) == nil {
item.Details = details
}
items = append(items, item)
}
return items
} }
// ResultHandler 结果处理器 // ResultHandler 结果处理器
@@ -169,7 +253,6 @@ func (h *ResultHandler) List(w http.ResponseWriter, r *http.Request) {
return return
} }
// 支持类型过滤
typeFilter := r.URL.Query().Get("type") typeFilter := r.URL.Query().Get("type")
items := h.store.List() items := h.store.List()
@@ -190,7 +273,7 @@ func (h *ResultHandler) List(w http.ResponseWriter, r *http.Request) {
}) })
} }
// ExportOutput 导出输出结构(与CLI格式一致) // ExportOutput 导出输出结构
type ExportOutput struct { type ExportOutput struct {
ScanTime time.Time `json:"scan_time"` ScanTime time.Time `json:"scan_time"`
Summary ExportSummary `json:"summary"` Summary ExportSummary `json:"summary"`
@@ -208,7 +291,7 @@ type ExportSummary struct {
TotalVulns int `json:"total_vulns"` TotalVulns int `json:"total_vulns"`
} }
// Export 导出结果(与CLI格式一致) // Export 导出结果
func (h *ResultHandler) Export(w http.ResponseWriter, r *http.Request) { func (h *ResultHandler) Export(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
@@ -222,7 +305,6 @@ func (h *ResultHandler) Export(w http.ResponseWriter, r *http.Request) {
items := h.store.List() items := h.store.List()
// 按类型分类
var hosts, ports, services, vulns []ResultItem var hosts, ports, services, vulns []ResultItem
for _, item := range items { for _, item := range items {
switch item.Type { switch item.Type {
@@ -262,7 +344,6 @@ func (h *ResultHandler) Export(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Disposition", "attachment; filename=fscan_results.csv") w.Header().Set("Content-Disposition", "attachment; filename=fscan_results.csv")
writer := csv.NewWriter(w) writer := csv.NewWriter(w)
// Hosts section
if len(hosts) > 0 { if len(hosts) > 0 {
writer.Write([]string{"# Hosts"}) writer.Write([]string{"# Hosts"})
writer.Write([]string{"Target"}) writer.Write([]string{"Target"})
@@ -272,7 +353,6 @@ func (h *ResultHandler) Export(w http.ResponseWriter, r *http.Request) {
writer.Write([]string{}) writer.Write([]string{})
} }
// Ports section
if len(ports) > 0 { if len(ports) > 0 {
writer.Write([]string{"# Ports"}) writer.Write([]string{"# Ports"})
writer.Write([]string{"Target", "Port", "Status"}) writer.Write([]string{"Target", "Port", "Status"})
@@ -284,7 +364,6 @@ func (h *ResultHandler) Export(w http.ResponseWriter, r *http.Request) {
writer.Write([]string{}) writer.Write([]string{})
} }
// Services section
if len(services) > 0 { if len(services) > 0 {
writer.Write([]string{"# Services"}) writer.Write([]string{"# Services"})
writer.Write([]string{"Target", "Service", "Version", "Banner"}) writer.Write([]string{"Target", "Service", "Version", "Banner"})
@@ -295,7 +374,6 @@ func (h *ResultHandler) Export(w http.ResponseWriter, r *http.Request) {
writer.Write([]string{}) writer.Write([]string{})
} }
// Vulns section
if len(vulns) > 0 { if len(vulns) > 0 {
writer.Write([]string{"# Vulns"}) writer.Write([]string{"# Vulns"})
writer.Write([]string{"Target", "Type", "Details"}) writer.Write([]string{"Target", "Type", "Details"})
@@ -394,19 +472,15 @@ func buildStatusFromDetails(resultType, originalStatus string, details map[strin
return "open" return "open"
case "service": case "service":
// 服务名
if name, ok := details["name"].(string); ok && name != "" { if name, ok := details["name"].(string); ok && name != "" {
parts = append(parts, name) parts = append(parts, name)
} }
// 版本
if version, ok := details["version"].(string); ok && version != "" { if version, ok := details["version"].(string); ok && version != "" {
parts = append(parts, version) parts = append(parts, version)
} }
// 产品
if product, ok := details["product"].(string); ok && product != "" { if product, ok := details["product"].(string); ok && product != "" {
parts = append(parts, product) parts = append(parts, product)
} }
// 系统
if os, ok := details["os"].(string); ok && os != "" { if os, ok := details["os"].(string); ok && os != "" {
parts = append(parts, os) parts = append(parts, os)
} }
@@ -415,7 +489,6 @@ func buildStatusFromDetails(resultType, originalStatus string, details map[strin
} }
case "vuln": case "vuln":
// 统一漏洞显示格式
return normalizeVulnStatus(originalStatus, details) return normalizeVulnStatus(originalStatus, details)
case "host": case "host":
@@ -427,7 +500,6 @@ func buildStatusFromDetails(resultType, originalStatus string, details map[strin
// normalizeVulnStatus 统一漏洞状态显示 // normalizeVulnStatus 统一漏洞状态显示
func normalizeVulnStatus(status string, details map[string]interface{}) string { func normalizeVulnStatus(status string, details map[string]interface{}) string {
// 英文转中文映射
vulnTranslations := map[string]string{ vulnTranslations := map[string]string{
"weak_credential": i18n.GetText("web_result_weak_credential"), "weak_credential": i18n.GetText("web_result_weak_credential"),
"unauthorized": i18n.GetText("unauthorized_access"), "unauthorized": i18n.GetText("unauthorized_access"),
@@ -436,21 +508,17 @@ func normalizeVulnStatus(status string, details map[string]interface{}) string {
"CVE": i18n.GetText("web_result_vulnerability"), "CVE": i18n.GetText("web_result_vulnerability"),
} }
// 处理 "weak_credential: user:pass" 格式
if strings.HasPrefix(status, "weak_credential:") { if strings.HasPrefix(status, "weak_credential:") {
cred := strings.TrimPrefix(status, "weak_credential:") cred := strings.TrimPrefix(status, "weak_credential:")
cred = strings.TrimSpace(cred) cred = strings.TrimSpace(cred)
return i18n.Tr("web_result_weak_credential_detail", cred) return i18n.Tr("web_result_weak_credential_detail", cred)
} }
// 处理其他已知格式
for eng, chn := range vulnTranslations { for eng, chn := range vulnTranslations {
if strings.Contains(strings.ToLower(status), strings.ToLower(eng)) { if strings.Contains(strings.ToLower(status), strings.ToLower(eng)) {
// 如果已经是中文格式,直接返回
if strings.Contains(status, chn) { if strings.Contains(status, chn) {
return status return status
} }
// 替换英文部分
return strings.Replace(status, eng, chn, 1) return strings.Replace(status, eng, chn, 1)
} }
} }
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -5,7 +5,7 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>web-ui</title> <title>web-ui</title>
<script type="module" crossorigin src="/assets/index-DR5QRXeN.js"></script> <script type="module" crossorigin src="/assets/index-5JoZok-U.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CTrwF18n.css"> <link rel="stylesheet" crossorigin href="/assets/index-CTrwF18n.css">
</head> </head>
<body> <body>