Compare commits

...

8 Commits

Author SHA1 Message Date
poem
e9a58e89aa 更新:前端表格字段命名,后端注释更新 2025-12-18 22:31:55 +08:00
rongxinrou
3d9d520dc7 新增dev下进行worker构建,并使用本地构建的worker镜像启动容器 2025-12-18 21:54:54 +08:00
yyhuni
8d814b5864 fix: 日志逻辑 2025-12-18 20:39:53 +08:00
rongxinrou
c16b7afabe 增加日志 2025-12-18 20:30:25 +08:00
rongxinrou
fa55167989 修复端口扫描IP时的问题 2025-12-18 20:16:21 +08:00
yyhuni
55a2762c71 fix: 证书兼容性 2025-12-18 18:53:28 +08:00
github-actions[bot]
5532f1e63a chore: bump version to v1.0.9 2025-12-18 10:51:53 +00:00
yyhuni
948568e950 更新注释 2025-12-18 18:42:37 +08:00
13 changed files with 102 additions and 65 deletions

View File

@@ -1 +1 @@
v1.0.8
v1.0.9

View File

@@ -8,13 +8,32 @@
2. 选择负载最低的 Worker可能是本地或远程
3. 本地 Worker直接执行 docker run
4. 远程 Worker通过 SSH 执行 docker run
5. 任务执行完自动销毁容器
5. 任务执行完自动销毁容器--rm
镜像版本管理:
- 版本锁定:使用 settings.IMAGE_TAG 确保 server 和 worker 版本一致
- 预拉取策略:安装时预拉取镜像,执行时使用 --pull=missing
- 本地开发:可通过 TASK_EXECUTOR_IMAGE 环境变量指向本地镜像
环境变量注入:
- Worker 容器不使用 env_file通过 docker run -e 动态注入
- 只注入 SERVER_URL容器启动后从配置中心获取完整配置
- 本地 WorkerSERVER_URL = http://server:{port}Docker 内部网络)
- 远程 WorkerSERVER_URL = http://{public_host}:{port}(公网地址)
任务启动流程:
1. Server 调用 execute_scan_flow() 等方法提交任务
2. select_best_worker() 从 Redis 读取心跳数据,选择负载最低的节点
3. _build_docker_command() 构建完整的 docker run 命令:
- 设置网络(本地加入 Docker 网络,远程不指定)
- 注入环境变量(-e SERVER_URL=...
- 挂载结果和日志目录(-v
- 指定执行脚本python -m apps.scan.scripts.xxx
4. _execute_docker_command() 执行命令:
- 本地subprocess.run() 直接执行
- 远程paramiko SSH 执行
5. docker run -d 立即返回容器 ID任务在后台执行
特点:
- 负载感知:任务优先分发到最空闲的机器
- 统一调度:本地和远程 Worker 使用相同的选择逻辑

View File

@@ -225,6 +225,13 @@ def _parse_and_validate_line(line: str) -> Optional[PortScanRecord]:
ip = line_data.get('ip', '').strip()
port = line_data.get('port')
logger.debug("解析到的主机名: %s, IP: %s, 端口: %s", host, ip, port)
if not host and ip:
host = ip
logger.debug("主机名为空,使用 IP 作为 host")
# 步骤 4: 验证字段不为空
if not host or not ip or port is None:
logger.warning(

View File

@@ -1,4 +1,8 @@
#!/bin/bash
# 目前采用github action自动版本构建
# git tag v1.0.9
# git push origin v1.0.9
# ============================================
# Docker Hub 镜像推送脚本
# 用途:构建并推送所有服务镜像到 Docker Hub

View File

@@ -101,6 +101,18 @@ services:
# SSL 证书挂载(方便更新)
- ./nginx/ssl:/etc/nginx/ssl:ro
# Worker扫描任务执行容器开发模式下构建
worker:
build:
context: ..
dockerfile: docker/worker/Dockerfile
image: docker-worker:latest
restart: "no"
volumes:
- /opt/xingrin/results:/app/backend/results
- /opt/xingrin/logs:/app/backend/logs
command: echo "Worker image built for development"
volumes:
postgres_data:

View File

@@ -79,10 +79,10 @@ ENV GOPATH=/root/go
ENV PATH=/usr/local/go/bin:$PATH:$GOPATH/bin
ENV GOPROXY=https://goproxy.cn,direct
# 5. 安装 uv超快的 Python 包管理器)
# 5. 安装 uv Python 包管理器)
RUN pip install uv --break-system-packages
# 安装 Python 依赖(使用 uv 并行下载,速度快 10-100 倍
# 安装 Python 依赖(使用 uv 并行下载)
COPY backend/requirements.txt .
RUN --mount=type=cache,target=/root/.cache/uv \
uv pip install --system -r requirements.txt --break-system-packages && \

View File

@@ -78,21 +78,21 @@ export function createIPAddressColumns(params: {
enableSorting: false,
enableHiding: false,
},
// IP 地址
// IP 列
{
accessorKey: "ip",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="IP 地址" />
<DataTableColumnHeader column={column} title="IP Address" />
),
cell: ({ row }) => (
<TruncatedCell value={row.original.ip} maxLength="ip" mono />
),
},
// 关联主机名
// host
{
accessorKey: "hosts",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="关联主机" />
<DataTableColumnHeader column={column} title="Hosts" />
),
cell: ({ getValue }) => {
const hosts = getValue<string[]>()
@@ -107,7 +107,7 @@ export function createIPAddressColumns(params: {
return (
<div className="flex flex-col gap-1">
{displayHosts.map((host, index) => (
<span key={index} className="text-sm font-mono">{host}</span>
<TruncatedCell key={index} value={host} maxLength="host" mono />
))}
{hasMore && (
<Badge variant="secondary" className="text-xs w-fit">
@@ -118,11 +118,11 @@ export function createIPAddressColumns(params: {
)
},
},
// 发现时间
// discoveredAt
{
accessorKey: "discoveredAt",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="发现时间" />
<DataTableColumnHeader column={column} title="Discovered At" />
),
cell: ({ getValue }) => {
const value = getValue<string | undefined>()
@@ -133,7 +133,7 @@ export function createIPAddressColumns(params: {
{
accessorKey: "ports",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="开放端口" />
<DataTableColumnHeader column={column} title="Open Ports" />
),
cell: ({ getValue }) => {
const ports = getValue<number[]>()
@@ -191,7 +191,7 @@ export function createIPAddressColumns(params: {
</PopoverTrigger>
<PopoverContent className="w-80 p-3">
<div className="space-y-2">
<h4 className="font-medium text-sm"> ({sortedPorts.length})</h4>
<h4 className="font-medium text-sm">All Open Ports ({sortedPorts.length})</h4>
<div className="flex flex-wrap gap-1 max-h-32 overflow-y-auto">
{sortedPorts.map((port, index) => (
<Badge

View File

@@ -267,7 +267,7 @@ export const createTargetColumns = ({
{
accessorKey: "name",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="目标名称" />
<DataTableColumnHeader column={column} title="Target Name" />
),
cell: ({ row }) => (
<TargetNameCell
@@ -282,7 +282,7 @@ export const createTargetColumns = ({
{
accessorKey: "type",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="类型" />
<DataTableColumnHeader column={column} title="Type" />
),
cell: ({ row }) => {
const type = row.getValue("type") as string | null

View File

@@ -188,7 +188,7 @@ export const createEngineColumns = ({
{
accessorKey: "name",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="引擎名称" />
<DataTableColumnHeader column={column} title="Engine Name" />
),
cell: ({ row }) => {
const name = row.getValue("name") as string

View File

@@ -180,7 +180,7 @@ export const createScheduledScanColumns = ({
{
accessorKey: "name",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="任务名称" />
<DataTableColumnHeader column={column} title="Task Name" />
),
cell: ({ row }) => {
const name = row.getValue("name") as string
@@ -216,7 +216,7 @@ export const createScheduledScanColumns = ({
{
accessorKey: "engineName",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="扫描引擎" />
<DataTableColumnHeader column={column} title="Scan Engine" />
),
cell: ({ row }) => {
const engineName = row.getValue("engineName") as string
@@ -283,7 +283,7 @@ export const createScheduledScanColumns = ({
{
accessorKey: "isEnabled",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="状态" />
<DataTableColumnHeader column={column} title="Status" />
),
cell: ({ row }) => {
const isEnabled = row.getValue("isEnabled") as boolean
@@ -308,7 +308,7 @@ export const createScheduledScanColumns = ({
{
accessorKey: "nextRunTime",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="下次执行" />
<DataTableColumnHeader column={column} title="Next Run" />
),
cell: ({ row }) => {
const nextRunTime = row.getValue("nextRunTime") as string | undefined
@@ -324,7 +324,7 @@ export const createScheduledScanColumns = ({
{
accessorKey: "runCount",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="执行次数" />
<DataTableColumnHeader column={column} title="Run Count" />
),
cell: ({ row }) => {
const count = row.getValue("runCount") as number
@@ -338,7 +338,7 @@ export const createScheduledScanColumns = ({
{
accessorKey: "lastRunTime",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="上次执行" />
<DataTableColumnHeader column={column} title="Last Run" />
),
cell: ({ row }) => {
const lastRunTime = row.getValue("lastRunTime") as string | undefined

View File

@@ -100,7 +100,7 @@ export const createSubdomainColumns = ({
{
accessorKey: "discoveredAt",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="发现时间" />
<DataTableColumnHeader column={column} title="Discovered At" />
),
cell: ({ getValue }) => {
const value = getValue<string | undefined>()

View File

@@ -95,7 +95,7 @@ export const commandColumns: ColumnDef<Command>[] = [
{
accessorKey: "displayName",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="名称" />
<DataTableColumnHeader column={column} title="Name" />
),
cell: ({ row }) => {
const displayName = row.getValue("displayName") as string
@@ -136,7 +136,7 @@ export const commandColumns: ColumnDef<Command>[] = [
{
accessorKey: "tool",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="所属工具" />
<DataTableColumnHeader column={column} title="Tool" />
),
cell: ({ row }) => {
const tool = row.original.tool
@@ -156,7 +156,7 @@ export const commandColumns: ColumnDef<Command>[] = [
{
accessorKey: "commandTemplate",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="命令模板" />
<DataTableColumnHeader column={column} title="Command Template" />
),
cell: ({ row }) => {
const template = row.getValue("commandTemplate") as string
@@ -192,7 +192,7 @@ export const commandColumns: ColumnDef<Command>[] = [
{
accessorKey: "description",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="描述" />
<DataTableColumnHeader column={column} title="Description" />
),
cell: ({ row }) => {
const description = row.getValue("description") as string
@@ -217,7 +217,7 @@ export const commandColumns: ColumnDef<Command>[] = [
{
accessorKey: "updatedAt",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="更新时间" />
<DataTableColumnHeader column={column} title="Updated At" />
),
cell: ({ row }) => (
<div className="text-sm text-muted-foreground">

View File

@@ -126,7 +126,7 @@ update_env_var() {
GENERATED_DB_PASSWORD=""
GENERATED_DJANGO_KEY=""
# 生成自签 HTTPS 证书(无域名场景)——兼容旧版 OpenSSL
# 生成自签 HTTPS 证书(使用容器,避免宿主机 openssl 兼容性问题)
generate_self_signed_cert() {
local ssl_dir="$DOCKER_DIR/nginx/ssl"
local fullchain="$ssl_dir/fullchain.pem"
@@ -140,41 +140,18 @@ generate_self_signed_cert() {
info "未检测到 HTTPS 证书正在生成自签证书localhost..."
mkdir -p "$ssl_dir"
# 创建临时配置文件(兼容 OpenSSL 1.0.2
local config_file="/tmp/openssl-selfsigned.cnf"
cat > "$config_file" << EOF
[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
prompt = no
[req_distinguished_name]
C = CN
ST = NA
L = NA
O = XingRin
CN = localhost
[v3_req]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = DNS:localhost,IP:127.0.0.1
EOF
if openssl req -x509 -nodes -newkey rsa:2048 -days 365 \
-keyout "$privkey" \
-out "$fullchain" \
# 使用容器生成证书,避免依赖宿主机 openssl 版本
if docker run --rm -v "$ssl_dir:/ssl" alpine/openssl \
req -x509 -nodes -newkey rsa:2048 -days 365 \
-keyout /ssl/privkey.pem \
-out /ssl/fullchain.pem \
-subj "/C=CN/ST=NA/L=NA/O=XingRin/CN=localhost" \
-config "$config_file" \
-extensions v3_req >/dev/null 2>&1; then
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1" \
>/dev/null 2>&1; then
success "自签证书已生成: $ssl_dir"
else
warn "自签证书生成失败(可能是 OpenSSL 版本过旧),请手动放置证书到 $ssl_dir"
warn "或者升级系统 OpenSSL或使用 Let's Encrypt 证书"
warn "自签证书生成失败,请手动放置证书到 $ssl_dir"
fi
# 清理临时配置文件
rm -f "$config_file"
}
# 自动为 docker/.env 填充敏感变量
@@ -252,7 +229,7 @@ show_summary() {
step "[1/3] 检查基础命令"
MISSING_CMDS=()
for cmd in git curl jq openssl; do
for cmd in git curl; do
if ! command -v "$cmd" >/dev/null 2>&1; then
MISSING_CMDS+=("$cmd")
warn "未安装: $cmd"
@@ -423,11 +400,29 @@ DOCKER_USER=$(grep "^DOCKER_USER=" "$DOCKER_DIR/.env" 2>/dev/null | cut -d= -f2)
DOCKER_USER=${DOCKER_USER:-yyhuni}
WORKER_IMAGE="${DOCKER_USER}/xingrin-worker:${APP_VERSION}"
info "正在拉取: $WORKER_IMAGE"
if docker pull "$WORKER_IMAGE"; then
success "Worker 镜像拉取完成"
# 开发模式下构建本地 worker 镜像
if [ "$DEV_MODE" = true ]; then
info "开发模式:构建本地 Worker 镜像..."
if docker compose -f "$DOCKER_DIR/docker-compose.dev.yml" build worker; then
# 设置 TASK_EXECUTOR_IMAGE 环境变量指向本地构建的镜像
update_env_var "$DOCKER_DIR/.env" "TASK_EXECUTOR_IMAGE" "docker-worker:latest"
success "本地 Worker 镜像构建完成,并设置为默认使用镜像"
else
warn "本地 Worker 镜像构建失败,将使用远程镜像"
info "正在拉取: $WORKER_IMAGE"
if docker pull "$WORKER_IMAGE"; then
success "Worker 镜像拉取完成"
else
warn "Worker 镜像拉取失败,扫描时会自动重试拉取"
fi
fi
else
warn "Worker 镜像拉取失败,扫描时会自动重试拉取"
info "正在拉取: $WORKER_IMAGE"
if docker pull "$WORKER_IMAGE"; then
success "Worker 镜像拉取完成"
else
warn "Worker 镜像拉取失败,扫描时会自动重试拉取"
fi
fi
# ==============================================================================