简介:这份资源是面向初级运维人员与网络安全研究者的漏洞扫描系统设计文档,围绕Python、Django与Docker技术栈,讲解如何构建一个低学习成本的B/S架构扫描平台,帮助缺乏专业安全技能的用户快速开展漏洞检测与风险排查。压缩包内共1个docx文件,约2.18MB,内容涵盖绪论、技术简介、系统分析与设计等章节,涉及用户认证、信息管理、漏洞扫描、日志文章与权限管理等模块,并给出Nmap集成与报告生成思路。文档从课题背景、国内外漏洞危害现状切入,逐步展开需求分析、逻辑架构与功能模块设计,适合作为课程设计、毕业设计或安全入门实践的参考方案。目前已有345人学习下载,读者可借此理解Django的MVT开发模式、Docker轻量虚拟化封装扫描工具的方法,以及B/S架构下权限控制与任务管理的实现路径,为后续动手搭建类似系统提供较完整的结构参照。
1. 从一份 docx 标题说起:Python 漏洞扫描系统到底该做成什么样
很多人第一次看到「基于 Python 的漏洞扫描系统的设计与实现」这个标题,脑子里浮现的是 nmap 套个壳、点一下按钮出一堆红字。真动手做过的人知道,难点从来不在扫描本身,而在扫描之前的目标管理、扫描之中的任务调度、扫描之后的漏洞归并与报告。nmap 负责端口和服务的探测,Python 负责把探测结果变成可存储、可查询、可复现的数据,Django 负责把这一切包装成一个能多人使用的 Web 系统,Docker 负责让这套东西在别人机器上也能跑起来。这四件事凑齐,才叫一个「系统」,否则只是一个脚本。
这篇文章面向的是想把这个标题真正落地的人:可能是课程设计要交东西的学生,也可能是想给内部资产做轻量巡检的运维。我会按「先能跑、再能存、再能看、最后能扩展」的顺序,把选型理由、目录结构、关键代码、参数含义和踩过的坑讲清楚。读完你应该能自己搭出一套最小可用的版本,并且知道哪些地方现在别碰、以后再说。
2. 技术选型与最小骨架:为什么是 Python + Django + Docker + Nmap
2.1 四个组件各自解决什么问题
先把职责划清楚,后面写代码才不会互相打架。nmap 是扫描引擎,它输出的是 XML 或普通文本,包含主机存活、开放端口、服务指纹、部分脚本探测结果。Python 是胶水层,负责调用 nmap、解析结果、做并发控制、把数据写进数据库。Django 是展示层和任务管理层,提供用户、资产、任务、漏洞、报告这几类模型,以及对应的增删改查接口。Docker 解决的是环境一致性,尤其是 nmap 在不同发行版上的版本差异和权限问题。
选 Django 而不是 Flask 或 FastAPI,理由很实际:这个系统天然需要用户体系、后台管理、ORM 和模板渲染,Django 自带 admin 能省掉大量重复劳动,django-rabc 这类权限方案也成熟。选 Docker 而不是裸装,是因为 nmap 需要原始套接字权限,容器里用--cap-add=NET_RAW或--network host就能解决,比在宿主机上折腾 sudo 干净得多。
2.2 项目目录结构
一个能长期维护的结构比「能跑就行」重要得多。我一般会这样组织:
vulnscan/ ├── docker-compose.yml ├── Dockerfile ├── requirements.txt ├── manage.py ├── vulnscan/ # Django 配置 │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── scanner/ # 扫描核心 app │ ├── models.py │ ├── tasks.py │ ├── nmap_runner.py │ ├── parser.py │ └── views.py ├── assets/ # 资产与漏洞展示 app └── templates/scanner只负责「怎么扫、怎么解析」,assets只负责「怎么展示、怎么导出」。两个 app 之间通过模型外键关联,不要互相 import 视图函数,否则后期改一处崩一片。
2.3 用 Docker 把 nmap 和 Django 装进同一个环境
Dockerfile 的关键不是装多少东西,而是把 nmap 和 Python 依赖固定住:
FROM python:3.11-slim # nmap 必须装,且版本要固定,不同版本 XML 字段有差异 RUN apt-get update && apt-get install -y --no-install-recommends \ nmap \ && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]python:3.11-slim体积小,--no-install-recommends避免拉进一堆无用包。nmap 不写版本号时默认装当前源里的版本,生产环境建议锁死,比如nmap=7.94+git20230807.3be01efb1-1,否则换台机器解析逻辑可能对不上。
docker-compose 里要处理权限和网络:
services: web: build: . ports: - "8000:8000" cap_add: - NET_RAW - NET_ADMIN volumes: - .:/app depends_on: - db db: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: rootpass MYSQL_DATABASE: vulnscan volumes: - db_data:/var/lib/mysql volumes: db_data:NET_RAW是 nmap 做 SYN 扫描的前提,NET_ADMIN在部分探测模式下需要。如果启动时报virtualization support not detected,那是 Docker Desktop 在 Windows 上的 WSL2 后端没开,和本项目无关,去 BIOS 开虚拟化、在 Docker Desktop 设置里启用 WSL2 即可。数据库用 MySQL 8.0 是因为 Django 的 JSONField 在 MySQL 8 上支持完善,存 nmap 原始结果很方便。
3. 扫描引擎落地:nmap 调用、结果解析与任务调度
3.1 用 Python 调 nmap 的两种方式与参数选择
调 nmap 有两条路:subprocess 直接调命令行,或者用 python-nmap 库。python-nmap 本质也是包 subprocess,但把结果转成了字典,省去自己解析 XML。我一般用 subprocess +-oX -输出 XML 到标准输出,再用xml.etree解析,因为这样对字段的控制最细,出问题也容易定位。
import subprocess import xml.etree.ElementTree as ET def run_nmap(target: str, ports: str = "1-1000", timing: str = "T4"): """ target: 单个 IP 或 CIDR,如 192.168.1.0/24 ports: 端口范围,默认扫 1-1000,全端口用 1-65535 timing: T0-T5,T4 是速度和准确性的常用折中 """ cmd = [ "nmap", "-sS", # SYN 半开扫描,需要 NET_RAW "-sV", # 服务版本探测 "-Pn", # 跳过主机存活探测,避免被防火墙拦 ICMP "-T4", # 时序模板 "-p", ports, "-oX", "-", # XML 输出到 stdout target, ] result = subprocess.run( cmd, capture_output=True, text=True, timeout=1800 ) if result.returncode != 0: raise RuntimeError(f"nmap failed: {result.stderr}") return ET.fromstring(result.stdout)-sS比-sT快且隐蔽,但必须有 root 或 NET_RAW 权限,容器里已经给了。-Pn在扫描内网资产时几乎是必须的,很多主机不响应 ping 但端口是开的。-T4是速度和稳定性的平衡点,-T5在丢包网络里会大量误报。timeout=1800是防止大网段扫描卡死,实际项目里应该按目标数量动态计算。
3.2 解析 XML 并落库:模型设计与字段含义
nmap 的 XML 结构是<nmaprun>下面挂<host>,每个 host 有<address>、<ports>、<hostscript>。解析时要处理「主机存活但无开放端口」和「端口开放但服务识别失败」两种情况。
from scanner.models import ScanTask, Host, Port def parse_and_save(root, task: ScanTask): for host_el in root.findall("host"): addr_el = host_el.find("address") if addr_el is None: continue ip = addr_el.get("addr") host, _ = Host.objects.get_or_create(task=task, ip=ip) for port_el in host_el.findall(".//port"): state_el = port_el.find("state") if state_el is None or state_el.get("state") != "open": continue service_el = port_el.find("service") Port.objects.update_or_create( host=host, port=int(port_el.get("portid")), defaults={ "protocol": port_el.get("protocol"), "service": service_el.get("name") if service_el is not None else "", "product": service_el.get("product") if service_el is not None else "", "version": service_el.get("version") if service_el is not None else "", }, )update_or_create保证重复扫描同一目标时不会产生重复端口记录。service字段存的是 nmap 的服务名,product和version是-sV探测出来的具体软件和版本,这两个字段是后续匹配 CVE 的关键。注意state只取open,filtered和closed不落库,否则数据量会爆炸。
3.3 任务调度:别让扫描阻塞 Web 请求
新手最容易犯的错是在视图函数里直接调run_nmap,结果页面转圈几分钟然后超时。正确做法是把扫描任务丢给后台 worker。轻量方案用threading+ 数据库状态字段,重一点用 Celery + Redis。
import threading from django.db import close_old_connections def start_scan_async(task_id: int): def _run(): close_old_connections() task = ScanTask.objects.get(id=task_id) task.status = "running" task.save() try: root = run_nmap(task.target, task.ports) parse_and_save(root, task) task.status = "done" except Exception as e: task.status = "failed" task.error = str(e) finally: task.save() close_old_connections() threading.Thread(target=_run, daemon=True).start()close_old_connections()是必须的,Django 的数据库连接不能跨线程复用,不加这行在并发扫描时会报MySQL server has gone away。daemon=True保证主进程退出时线程不会挂住。这个方案适合单机小规模,任务多了要换成 Celery,否则线程数不可控。
4. 避坑与排查:那些让扫描系统翻车的细节
4.1 扫描结果全是 filtered 或直接超时
现象:任务状态变成 done,但主机列表是空的,或者所有端口都是 filtered。原因通常是容器网络模式不对,或者目标网段有防火墙。先确认容器用的是--network host还是默认 bridge,bridge 模式下 nmap 看到的源 IP 是容器网段,很多内网 ACL 会直接丢弃。解决:扫描内网时用network_mode: host,或者把容器放到目标同网段。另外检查-Pn是否加上,没加的话目标不回 ICMP 就整个跳过。
4.2 解析 XML 时报编码错误
现象:ET.fromstring抛ValueError: Unicode strings with encoding declaration are not supported。原因是 nmap 输出的 XML 第一行带<?xml version="1.0" encoding="UTF-8"?>,而fromstring收到的是 str 不是 bytes。解决:把result.stdout改成result.stdout.encode("utf-8")再传进去,或者用ET.XMLParser(encoding="utf-8")。这个坑在 python-nmap 里被封装掉了,自己解析就一定会遇到。
4.3 Docker 里 nmap 报权限不足
现象:Failed to open device eth0或You requested a scan type which requires root privileges。原因是容器默认没有 NET_RAW capability。解决:compose 里加cap_add: [NET_RAW, NET_ADMIN],或者临时用docker run --cap-add=NET_RAW。不要用--privileged,那等于把宿主机权限全交出去,没必要。
4.4 扫描大网段时内存暴涨
现象:扫/16网段时进程被 OOM kill。原因是subprocess.run把整个 XML 读进内存,几万台主机的 XML 能到几百 MB。解决:改用subprocess.Popen流式读取,或者把大网段拆成/24分批扫描,每批落库后释放。nmap 本身有--max-hostgroup和--min-parallelism可以控制并发,但内存问题主要在 Python 侧。
4.5 重复扫描产生重复漏洞记录
现象:同一个资产扫两次,漏洞列表翻倍。原因是漏洞匹配逻辑没有做去重。解决:在漏洞模型上加unique_together = ("host", "port", "vuln_id"),或者用update_or_create按vuln_id更新。更稳妥的做法是每次扫描生成一个scan_id,展示时只取最新一次的结果,历史记录保留用于对比。
5. 从能跑到好用:漏洞匹配、报告导出与一个我常用的验证习惯
系统能扫出端口和服务之后,下一步是把它变成「漏洞扫描」而不是「端口扫描」。最轻量的做法是维护一张本地漏洞规则表,字段包括service、product、version_range、cve_id、severity、description。扫描完成后用product和version去匹配,命中就生成漏洞记录。规则表可以手工维护几十条高频的,比如 OpenSSH 旧版本、Apache 特定版本、Redis 未授权。不要一上来就接 NVD 全量库,几万条规则匹配性能很差,而且误报率高。
from packaging.version import parse as vparse def match_vulns(port): rules = VulnRule.objects.filter(service=port.service) hits = [] for rule in rules: if rule.product and rule.product.lower() not in port.product.lower(): continue if rule.version_max and vparse(port.version) > vparse(rule.version_max): continue hits.append(rule) return hitspackaging.version比字符串比较靠谱,能正确处理1.10 > 1.9这种。version_max表示「小于等于该版本受影响」,规则表里存边界值。匹配时先过滤 service,再过滤 product,最后比版本,三层过滤能把候选规则从几万降到几条。
报告导出用 Django 的模板渲染成 HTML,再交给浏览器打印成 PDF,比直接生成 PDF 省事得多。模板里按主机分组,每个主机下列端口和漏洞,严重级别用颜色区分。导出接口加一个?task_id=参数,只导出指定任务的结果。
最后说一个我自己的验证习惯:每次改完解析逻辑或匹配规则,不要只看新扫描的结果,一定要拿一份固定的 nmap XML 样本跑回归。样本里故意包含「主机存活无端口」「端口开放服务识别失败」「版本号带字母后缀」这几种边界情况。跑一遍看解析出的记录数和字段值是否和预期一致。这个习惯帮我挡掉过好几次「改了一行解析,结果所有版本号都变成空」的事故。扫描系统这种东西,数据一旦落库错了,后面所有报告都是错的,后悔药没得吃。
希望帮到你。
本文还有配套的精品资源,点击获取