From db6c31fdbb021167c6cb144887a7f4e3df1ce0bb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 14 Aug 2025 11:27:41 +0000 Subject: [PATCH 1/3] Initialize SVScan: modular network and vulnerability scanner Co-authored-by: egasuper2 --- .dockerignore | 14 ++ Dockerfile | 8 + LICENSE | 19 ++ Makefile | 25 +++ README.md | 75 +++++++- pyproject.toml | 43 +++++ svscan.egg-info/PKG-INFO | 23 +++ svscan.egg-info/SOURCES.txt | 21 +++ svscan.egg-info/dependency_links.txt | 1 + svscan.egg-info/entry_points.txt | 2 + svscan.egg-info/requires.txt | 1 + svscan.egg-info/top_level.txt | 1 + svscan/__init__.py | 1 + svscan/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 181 bytes svscan/__pycache__/cli.cpython-313.pyc | Bin 0 -> 5324 bytes svscan/__pycache__/version.cpython-313.pyc | Bin 0 -> 147 bytes svscan/cli.py | 91 ++++++++++ svscan/data/__init__.py | 0 svscan/data/vuln_db.json | 34 ++++ svscan/reporting/__init__.py | 0 svscan/reporting/serialize.py | 6 + svscan/scanners/__init__.py | 0 .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 135 bytes .../network_scanner.cpython-313.pyc | Bin 0 -> 6280 bytes .../__pycache__/vuln_scanner.cpython-313.pyc | Bin 0 -> 10954 bytes svscan/scanners/network_scanner.py | 113 ++++++++++++ svscan/scanners/vuln_scanner.py | 169 ++++++++++++++++++ svscan/utils/__init__.py | 0 svscan/utils/targets.py | 7 + svscan/version.py | 1 + 30 files changed, 653 insertions(+), 2 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 pyproject.toml create mode 100644 svscan.egg-info/PKG-INFO create mode 100644 svscan.egg-info/SOURCES.txt create mode 100644 svscan.egg-info/dependency_links.txt create mode 100644 svscan.egg-info/entry_points.txt create mode 100644 svscan.egg-info/requires.txt create mode 100644 svscan.egg-info/top_level.txt create mode 100644 svscan/__init__.py create mode 100644 svscan/__pycache__/__init__.cpython-313.pyc create mode 100644 svscan/__pycache__/cli.cpython-313.pyc create mode 100644 svscan/__pycache__/version.cpython-313.pyc create mode 100644 svscan/cli.py create mode 100644 svscan/data/__init__.py create mode 100644 svscan/data/vuln_db.json create mode 100644 svscan/reporting/__init__.py create mode 100644 svscan/reporting/serialize.py create mode 100644 svscan/scanners/__init__.py create mode 100644 svscan/scanners/__pycache__/__init__.cpython-313.pyc create mode 100644 svscan/scanners/__pycache__/network_scanner.cpython-313.pyc create mode 100644 svscan/scanners/__pycache__/vuln_scanner.cpython-313.pyc create mode 100644 svscan/scanners/network_scanner.py create mode 100644 svscan/scanners/vuln_scanner.py create mode 100644 svscan/utils/__init__.py create mode 100644 svscan/utils/targets.py create mode 100644 svscan/version.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..150afb3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd +.build/ +.env +.venv +.git +.gitignore +.dist/ +**/.mypy_cache/ +**/.pytest_cache/ +**/.ruff_cache/ +**/.DS_Store \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..380b1ec --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM python:3.12-slim + +WORKDIR /app +COPY . /app +RUN pip install --no-cache-dir . + +ENTRYPOINT ["svscan"] +CMD ["--help"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6802bc4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..23de295 --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +PYTHON ?= python3 +PIP ?= $(PYTHON) -m pip + +.PHONY: install dev uninstall help build docker-build docker-run + +help: + @echo "Targets: install, dev, uninstall, build, docker-build, docker-run" + +install: + $(PIP) install . + +dev: + $(PIP) install -e . + +uninstall: + -$(PIP) uninstall -y svscan + +build: + $(PYTHON) -m build + +docker-build: + docker build -t svscan:latest . + +docker-run: + docker run --rm svscan:latest --help \ No newline at end of file diff --git a/README.md b/README.md index 2996366..3ff22e2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,73 @@ -# NetNinjaScan -Сканер сети и уязвимостей +# SVScan — модульный сканер сети и уязвимостей + +Небольшой консольный аналог Nessus/OpenVAS/NetScanPro: быстрый сетевой сканер (TCP connect), лёгкое фингерпринтинг‑сканирование и локальная база сигнатур уязвимостей. + +## Возможности (MVP) +- Сканер сети: асинхронный TCP connect, CIDR/hostname/IP, настраиваемые порты и параллелизм +- Сканер уязвимостей: базовый фингерпринтинг HTTP/SSH/FTP/SMTP и сопоставление с локальной БД (`svscan/data/vuln_db.json`) +- Удобный CLI с подкомандами: `svscan net scan`, `svscan vuln scan` +- Простая установка: Docker и Python пакет + +## Установка + +### Вариант 1: Docker (самый быстрый) +```bash +docker build -t svscan:latest . +docker run --rm svscan:latest --help +``` + +### Вариант 2: pipx (рекомендуется для локальной установки) +После публикации на GitHub: +```bash +pipx install git+https://github.com//svscan +svscan --help +``` +Локально из исходников: +```bash +pipx install /workspace +svscan --help +``` + +### Вариант 3: pip (в пользовательскую директорию) +```bash +python3 -m pip install -e . --user +~/.local/bin/svscan --help +``` + +## Примеры использования +- Скан сети (диапазон): +```bash +svscan net scan 192.168.1.0/24 -p 22,80,443 --json +``` +- Скан уязвимостей (фингерпринтинг + локальные сигнатуры): +```bash +svscan vuln scan example.com -p 21,22,25,80,443 -o report.json +``` + +## Архитектура +- `svscan/cli.py` — CLI (Typer), подкоманды `net` и `vuln` +- `svscan/scanners/network_scanner.py` — сетевой сканер (async TCP connect), парсинг целей/портов +- `svscan/scanners/vuln_scanner.py` — фингерпринтинг (HTTP/SSH/FTP/SMTP) + сопоставление с БД +- `svscan/data/vuln_db.json` — минимальная локальная БД сигнатур +- Пакетирование: `pyproject.toml` (entrypoint `svscan`) +- Контейнеризация: `Dockerfile` + +Модульность: код сканера сети и уязвимостей расположен в отдельных папках. Легко добавлять новые модули (например, `svscan/scanners/udp_scanner.py`). + +## Как развивать +- Новые протоколы: SMB, RDP, TLS (шифросьюты), SNMP, Redis, MySQL/PostgreSQL +- UDP‑сканирование, ОС‑детектирование, баннер‑граббинг с эвристиками, HTTP‑краулер +- Экспорт: JSON/SARIF/JUnit, интеграции Jira/GitLab +- Планировщик, кэш, продолжение прерванных сканов, лимиты скорости +- Автообновление БД уязвимостей из NVD/OSV/CISA KEV, расчёт CVSS +- TUI/Web‑UI, REST API, плагинная система (SDK для правил/сценариев) + +## Монетизация (идеи) +- Freemium: базовый скан бесплатный; Pro — расширенные плагины, Web‑UI, планировщик, отчёты, интеграции +- Подписка на обновления сигнатур/отчётов (обогащение данными, приоритетные обновления) +- Коммерческая поддержка/SLAs, аудит, консультации +- Marketplace плагинов (корпоративные правила, интеграции SIEM/SOAR) +- Cloud SaaS‑версия: сканы через управляемую облачную очередь/агенты + +## Дисклеймер +Используйте инструмент только на ресурсах, на тест‑стендах или при наличии письменного разрешения. Ответственность за использование лежит на пользователе. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c617ac5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,43 @@ +[build-system] +requires = ["setuptools>=68.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "svscan" +version = "0.1.0" +description = "Modular security scanner (network + vulnerability) with fast, easy install." +readme = "README.md" +requires-python = ">=3.10" +license = { text = "MIT" } +authors = [ + { name = "SVScan Authors", email = "maintainers@example.com" } +] +keywords = ["security", "scanner", "network", "vulnerability", "cli"] +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Topic :: Security", + "Intended Audience :: System Administrators", + "Intended Audience :: Developers" +] +dependencies = [ + "typer>=0.12.3" +] + +[project.urls] +Homepage = "https://github.com/example/svscan" +Issues = "https://github.com/example/svscan/issues" + +[project.scripts] +svscan = "svscan.cli:app" + +[tool.setuptools] +include-package-data = true + +[tool.setuptools.packages.find] +where = ["."] +include = ["svscan*"] + +[tool.setuptools.package-data] +"svscan.data" = ["*.json"] \ No newline at end of file diff --git a/svscan.egg-info/PKG-INFO b/svscan.egg-info/PKG-INFO new file mode 100644 index 0000000..75a20de --- /dev/null +++ b/svscan.egg-info/PKG-INFO @@ -0,0 +1,23 @@ +Metadata-Version: 2.4 +Name: svscan +Version: 0.1.0 +Summary: Modular security scanner (network + vulnerability) with fast, easy install. +Author-email: SVScan Authors +License: MIT +Project-URL: Homepage, https://github.com/example/svscan +Project-URL: Issues, https://github.com/example/svscan/issues +Keywords: security,scanner,network,vulnerability,cli +Classifier: Programming Language :: Python :: 3 +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Classifier: Topic :: Security +Classifier: Intended Audience :: System Administrators +Classifier: Intended Audience :: Developers +Requires-Python: >=3.10 +Description-Content-Type: text/markdown +License-File: LICENSE +Requires-Dist: typer>=0.12.3 +Dynamic: license-file + +# NetNinjaScan +Сканер сети и уязвимостей diff --git a/svscan.egg-info/SOURCES.txt b/svscan.egg-info/SOURCES.txt new file mode 100644 index 0000000..703b3f8 --- /dev/null +++ b/svscan.egg-info/SOURCES.txt @@ -0,0 +1,21 @@ +LICENSE +README.md +pyproject.toml +svscan/__init__.py +svscan/cli.py +svscan/version.py +svscan.egg-info/PKG-INFO +svscan.egg-info/SOURCES.txt +svscan.egg-info/dependency_links.txt +svscan.egg-info/entry_points.txt +svscan.egg-info/requires.txt +svscan.egg-info/top_level.txt +svscan/data/__init__.py +svscan/data/vuln_db.json +svscan/reporting/__init__.py +svscan/reporting/serialize.py +svscan/scanners/__init__.py +svscan/scanners/network_scanner.py +svscan/scanners/vuln_scanner.py +svscan/utils/__init__.py +svscan/utils/targets.py \ No newline at end of file diff --git a/svscan.egg-info/dependency_links.txt b/svscan.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/svscan.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/svscan.egg-info/entry_points.txt b/svscan.egg-info/entry_points.txt new file mode 100644 index 0000000..7d47d17 --- /dev/null +++ b/svscan.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +svscan = svscan.cli:app diff --git a/svscan.egg-info/requires.txt b/svscan.egg-info/requires.txt new file mode 100644 index 0000000..ffe491a --- /dev/null +++ b/svscan.egg-info/requires.txt @@ -0,0 +1 @@ +typer>=0.12.3 diff --git a/svscan.egg-info/top_level.txt b/svscan.egg-info/top_level.txt new file mode 100644 index 0000000..8c6f5cc --- /dev/null +++ b/svscan.egg-info/top_level.txt @@ -0,0 +1 @@ +svscan diff --git a/svscan/__init__.py b/svscan/__init__.py new file mode 100644 index 0000000..14a0d54 --- /dev/null +++ b/svscan/__init__.py @@ -0,0 +1 @@ +from .version import VERSION as __version__ \ No newline at end of file diff --git a/svscan/__pycache__/__init__.cpython-313.pyc b/svscan/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8ecb26886e450e484f33d30f6264de18edd71771 GIT binary patch literal 181 zcmey&%ge<81OmtBW@rQH#~=<2FhLog1%QmH48aV+jNS}hj73Ze4Czdo%r6;%!kUb? z*uz|df<68H{4|+wv6rP56=&w>6)^+lZgI!QL;3OXD;Yk6jJqYPU!Gr-U0jfuoT^`3 zR-Bxeryn1mnU`4-AFo$Xd5gm)H$SB`C)KWq6Q~+wLoo-C_`uA_$as%IyNDgg0RZ8w BD{lY* literal 0 HcmV?d00001 diff --git a/svscan/__pycache__/cli.cpython-313.pyc b/svscan/__pycache__/cli.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0bef0bebb1723394d21385f9e3f46432e1ff7016 GIT binary patch literal 5324 zcmeHLO>7&-6`tiTxg?iAq9lqYMVqT1TVftK zuy_!+_|Qw7UMFpFXY?E7v1>n59|HRJZ_T4~U~wyM1C0&P|CrvoRYHF|cH<800U7Rr zGCQ#Mp>z0f=R^A$?8iItu7|D>z#{Gf33k+kyW?!U!w72Po_GiDjeGF!_zt`$&NW*5 z;$G~;%%}tRQ!l;5H2vg@xBSO`3O>|^Z{w%O{%8F(aF3rl(@ZdQmj=RA(4cZBGjdTi zpDGkH`CK9!WK{ml(I`G~igw!MWFj|POpvrxlvSaSAhI-D$djTBa&}V6&7Lb|bF)kD@lFHHbRTGhlC6Y^0 z+x3qF25<;%J_+OvG^4kpsG-R=88|b{3>~3;4m4uW&!C9Xh9IkW@qGTgcupcRcvH;e z=JI4QK|LE*c=;SPMw|+=smhA)GLEhnRt}!7@84nZ#I!sp z&gbPK^$D)Vk8JBz$+CQp9PAgpzsK;UqO(EM^v^e5ok*%M!H07T$di z1Qp+8;wP}JxnzkDDOaRyV}z`jLm?PZC>3GByyE{6$rL4#NOCD#l*M9RoXcb-vHvAG zp9>ySOra3vl=f+ofq5eM)D)GGiz%2x<@NA2YHC8gu8t}+*G^tgP4(qeJ|WN2$&x0` znmVySp~c8J#011h19uUMG5Y@JOf$`Ri8do-YmRO-!|S!@OwaXTW{hi&I%?mkr-I$SRjg6cZ{lrwUZ# z*?KbxQgKc@tTcqh?H_5Uoo0Y`?b0C!p-QKht3Ut@+mXw^dUT_ud+Ee&hx^LH<%PEg z*UsNOSn;Jc9MaP9PkF~L_S|;1T~RJ8t9|QT73bh3?qk7q+vQ%KTTQMX|Ep{C22*Wu zt?;k&tK84+U$Urm|EEIhZJX;#lrjC|tXvpjyg-MbRG8d>wK zx38PZfuRrD_inOiXW!>`nEXotmVVzg`ju=0COuy~FHs6ThKO;iEmYffU zNdR0zL`u3S>7k^TlHEY2NFQbChN)(lg#xgw9ME8PyAByiu4Ap(e>NdQss!m!HlIvn zMLCnsB_JD>-t zRT%$Jd?)+p&|ykOC>f;iMZyvAsqz%X1 ze;Lf({!8OqV9u?bdDpi3)3PUg%QjSP^Q;_t{m^RU!?wOkTVE5#@vWBAe{0!M?F_70 zZ*>kV^NpTrhj+Da?Xik4^r3I0;v3oU?Js)|Tt9ombF=;TJ8w8|7T-HwKK(-FE-li`$5el(s;pcjs&OuxPZU)aPOr+H&F8aP zIzVU`5t^E6ub@W8@Pzg#m7XLRIv~7|9DpmZ#xe}^5psNlgukP%Pmt{&sQXI;%NYJ? zM$ExGCgI}3s|#zr<N9&M zv#)A*E%&_ke2oSEj^L;np#&uzZ8e^9X4LAgStw^kcK0%VZL%iNnhiNxYjk|gflSsf zoWN02yfp)Ddjc7(A2O~A<67Rc!FabB6RUX7d{4&gVp^NV0IwMXWy2WC)s3OtmNB$e UH->U`V}PT^_&zfLrv=cz0ixJ2KczG$)vkyQ Zs2F5=G05@{%#4hTH@E~F*^5|!TmZDqAr$}s literal 0 HcmV?d00001 diff --git a/svscan/cli.py b/svscan/cli.py new file mode 100644 index 0000000..641f083 --- /dev/null +++ b/svscan/cli.py @@ -0,0 +1,91 @@ +import asyncio +import json +from typing import List, Optional + +import typer + +from svscan.version import VERSION +from svscan.scanners.network_scanner import ( + scan_targets, + parse_ports, +) +from svscan.scanners.vuln_scanner import run_vuln_scan + +app = typer.Typer(no_args_is_help=True, help="SVScan: modular network and vulnerability scanner") + +net_app = typer.Typer(help="Network scanning commands") +vuln_app = typer.Typer(help="Vulnerability scanning commands") + +app.add_typer(net_app, name="net") +app.add_typer(vuln_app, name="vuln") + + +@app.command() +def version() -> None: + """Show version information.""" + typer.echo(f"svscan {VERSION}") + + +@net_app.command("scan") +def net_scan( + targets: List[str] = typer.Argument(..., help="Targets: IPs, hostnames, or CIDRs (e.g. 192.168.1.0/24)"), + ports: str = typer.Option("22,80,443", "--ports", "-p", help="Comma-separated ports"), + timeout: float = typer.Option(0.5, help="Per-connection timeout (seconds)"), + concurrency: int = typer.Option(200, help="Max concurrent connections"), + output: Optional[str] = typer.Option(None, "--output", "-o", help="Write results to file (json)"), + as_json: bool = typer.Option(False, "--json", help="Print JSON to stdout"), +) -> None: + """Run a TCP connect network scan.""" + port_list = parse_ports(ports) + results = asyncio.run(scan_targets(targets, port_list, timeout=timeout, concurrency=concurrency)) + + if as_json or output: + data = json.dumps(results, indent=2) + if output: + with open(output, "w", encoding="utf-8") as f: + f.write(data) + if as_json: + typer.echo(data) + else: + for host_result in results: + typer.echo(f"Host: {host_result['host']}") + open_ports = host_result.get("open_ports", []) + if not open_ports: + typer.echo(" No open ports found") + else: + for port in open_ports: + typer.echo(f" Port {port}") + + +@vuln_app.command("scan") +def vuln_scan( + targets: List[str] = typer.Argument(..., help="Targets: IPs, hostnames, or CIDRs"), + ports: str = typer.Option("22,80,443", "--ports", "-p", help="Comma-separated ports"), + timeout: float = typer.Option(0.8, help="Per-connection timeout (seconds)"), + concurrency: int = typer.Option(200, help="Max concurrent connections"), + output: Optional[str] = typer.Option(None, "--output", "-o", help="Write results to file (json)"), + as_json: bool = typer.Option(False, "--json", help="Print JSON to stdout"), +) -> None: + """Run a vulnerability scan (network scan + lightweight fingerprinting + DB match).""" + port_list = parse_ports(ports) + results = asyncio.run(run_vuln_scan(targets, port_list, timeout=timeout, concurrency=concurrency)) + + if as_json or output: + data = json.dumps(results, indent=2) + if output: + with open(output, "w", encoding="utf-8") as f: + f.write(data) + if as_json: + typer.echo(data) + else: + for host in results: + typer.echo(f"Host: {host['host']}") + if not host.get("findings"): + typer.echo(" No vulnerabilities found (based on local signatures)") + continue + for finding in host["findings"]: + typer.echo( + f" [{finding['severity'].upper()}] {finding['id']} on {finding['service']}\n" + f" Evidence: {finding.get('evidence', '')}\n" + f" Description: {finding.get('description', '')}" + ) \ No newline at end of file diff --git a/svscan/data/__init__.py b/svscan/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/svscan/data/vuln_db.json b/svscan/data/vuln_db.json new file mode 100644 index 0000000..7502e67 --- /dev/null +++ b/svscan/data/vuln_db.json @@ -0,0 +1,34 @@ +[ + { + "id": "CVE-2021-41773", + "service": "http", + "match": { "type": "header_regex", "header": "server", "pattern": "Apache/2\\.4\\.49" }, + "severity": "high", + "description": "Apache HTTP Server 2.4.49 path traversal and RCE in default configs.", + "references": ["https://httpd.apache.org/security/vulnerabilities_24.html#CVE-2021-41773"] + }, + { + "id": "CVE-2016-0777", + "service": "ssh", + "match": { "type": "banner_regex", "pattern": "OpenSSH_7\\.[01]" }, + "severity": "medium", + "description": "OpenSSH roaming vulnerability leads to potential key leak.", + "references": ["https://www.openssh.com/txt/release-7.1p2"] + }, + { + "id": "CVE-2019-10149", + "service": "smtp", + "match": { "type": "banner_regex", "pattern": "Exim 4\\.92" }, + "severity": "high", + "description": "Exim 4.92 local privilege escalation via string expansion flaw.", + "references": ["https://nvd.nist.gov/vuln/detail/CVE-2019-10149"] + }, + { + "id": "CVE-2011-2523", + "service": "ftp", + "match": { "type": "banner_regex", "pattern": "vsFTPd 2\\.3\\.4" }, + "severity": "critical", + "description": "vsftpd 2.3.4 backdoor allows remote code execution.", + "references": ["https://nvd.nist.gov/vuln/detail/CVE-2011-2523"] + } +] \ No newline at end of file diff --git a/svscan/reporting/__init__.py b/svscan/reporting/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/svscan/reporting/serialize.py b/svscan/reporting/serialize.py new file mode 100644 index 0000000..8cd8276 --- /dev/null +++ b/svscan/reporting/serialize.py @@ -0,0 +1,6 @@ +import json +from typing import Any + + +def to_json(data: Any) -> str: + return json.dumps(data, indent=2) \ No newline at end of file diff --git a/svscan/scanners/__init__.py b/svscan/scanners/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/svscan/scanners/__pycache__/__init__.cpython-313.pyc b/svscan/scanners/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..15b5d0abe9184c5053c4e4799ac68ef61d5b95b1 GIT binary patch literal 135 zcmey&%ge<81QN&RW`O9&AOZ#$p^VQgK*m&tbOudEzm*I{OhDdekklx?BasN z2KczG$)vkyYs10OnF^KVznURsP Ih#ANN0B58gRsaA1 literal 0 HcmV?d00001 diff --git a/svscan/scanners/__pycache__/network_scanner.cpython-313.pyc b/svscan/scanners/__pycache__/network_scanner.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ed2cb94bf99d75048a1b767a10c1577c21e33ad3 GIT binary patch literal 6280 zcmai2eNbChcE9gS-$Fu2eA|8)u%pC=O#yEl+a!R$zyT9I$#D#}{16YU7$oIB;n>qrA|pce?-hKY4NMB+VZ^=Sojt zmUb`b-gD1A_v_sAJLlfF=5{*>q#eKc`wN1bkpIMrnTqnn+Up!4H;FQ-$1MT))O-tUos*DNZIO7fmay^6CRT9C(D83nB_)>^E%p;uAANAdLg zpuMEorg&M4pS2X5-A?dNDcvydi=+5qHe9Epzo_5Y z%;U<~iXBP-M|h8;Wy4&s>3gCxDkXmxOqYS1DY zw$8Lh)qxSs5Z{XHX+u05kEIQvEpa6%JSl~-p7cT%|2H_67!GYFAy2G*4~UzjpWq=; zUg4-vPx{4vN#S=xOTVSxipzw$fOSQrwqYq~F^Y~S^|TUG6Cq6>8%aNLLZcyG0H)zc zrnE#fm85C?4s6A4*rL&dI;usZh9epsO%9HYAnl4q-yc&)@-3cdbSO^s^hi9RC6drA z=xJ&IS4||6X*C^BCiG~OT5%d7o=6*kXjDgjEUKo{G(Iqv)=;1r1lHh3jJ=JhE~LT0%y|9N%60oAv4U2bGKQUP zFRIdE!$P(67)``L!-Zr63N^eoz~InG5?q9YHcI406|N=EEg{E$Oh|%{@FDYT3d0d% z8a%eYl;;lu7`m(BOn`R)cxoph(0i*{cttRwGX}j<&3$;NEJXx!1qo>bS1fQX5iW#k z-<107Cgb!eMc4}!a|W}gOu8?^intJJbW_?e6K~3!CG`7<5GRpIA07%zeIApF6f|ba z*O8$3VQa5Sbxn>XM@QAYx|UL@n$`woc1$wZOHx*49ZV8B3~8i}W&0K-bra$p)3?Q&X63>p$u6T_NeQBz>#gN7wHCRnSM4q7Rk z1~DWysUgA|(uN(3l%~N{sbPfI6vKjl0o{1x?zQFYGjXe_`8CUVtTa$17G%zzZ*Y?@)az(=Zq>{e!%DZ(*SSpLv5<*SlI3m0PX1$+J2oWJvFlU%MpUg2~=4~*u_~**e|?WfHM%1hv)f+LaV4Tn!J30@CbQ8}ef;)R-Iwod1A4=R%5dTZ-nvN!vkWC;aqp zAsZ)I2dStUKl-KHKfz^O?x|yw$F6lwa9?@6;6Ac^u|4qE_>9wi?R9ilHrJGG(l+h;YgfirH07Rj zPY*n@RXnv5PXHF`@{M|K4JFr?+vr^UgpsI16Tdw+xX_0^DnE;$aDOP_mp zcf2ls{<{CL-}>t!4miIq;gI%=KvN7n;L9O&fVYc}YRR#5tl)#74MY7^_^n+6zg7!Y zeUtP-HJ3tw<;`j5dN3CNe`rT6g$k6(!BgAN4w%`THnie8HYGD4^XUD=t-|p7^#g)! zp?9d@CkiZB33%B;5MJk>elx-~^X5$GllSwTaL2y@I=aN&6nZ2V=mdN?`!nTaGA!6g zPf-ooLkia!(BBY`UO|Sx;k-uxUsCW(;5T4Ylo&2}ith_UTu|%*fYQUTsxVwPt*U+{ z5sN1cFJLZ8#}WyEeRL$5OwoNHVp!XbMmu|sgbk;1vg1Tl35SlfbsJ8I-~%H7RuDF9 zAE@zkbO`Y4Li|Q!Nw_z{z1n`{a!gC%Wi*(x4B7!BvY8DLX8>@5ypC>wfifS{qbZsk z$OU!U3vF;yBl;9%PXLdrPTz{N_JOl@*;zk+ILlk4z^c1s>g>m7f4c9}*4wTBvTx;; zLrbq5dgyMwSC?^lS6tOguIib{Ls#vpuVTfw^MP;YZ1YNO^HOc|eE447-Gj@u#}`BI zF8R)^xXvuP&SXl;o?A)L8;kDN-@DE{mw^0tckBN=vk_Ndjc`)n3y(i5K>3}14%YOe z()Rt_?WXo1S#TX_FBk4xx%N`aeVc@|+Xnpm0S-j&mvYD{ceL-a-rp%8zn<}TF*@jI z-zVH};!(1NLwcV8G!4QOcwZWgZ8Uh)*j4zg-Gl5LIme$9&WY!w9H1J>sHkx31`2^6 zo_Y}y5hFr`hi9DtPbk}a;;7VD0NmDaib&0zBBC^gUGs{t4-RCr$%e(Pf~(mB;8M8% zhDlgdr2Tvy_=2A8%pfg22A866?I3@0GrEDB{0g#GF}Jy}$dOKXe7%U6K0re*h4}~x zOD$mem^LpI?pBLMbZSnwPwg# zxts;C+c7+K5?q)EKU;X<5Rn1l$8?tFdh>{S}(eLN6UJnoNg_l}$g7%yG9{W{W?4ZRWL~N50W<$yWpA+#P=X z(7^lQtB4+j%&-ir=?fY~*GJRr2@R)>p@Ko39c}KA*>SU_;z^&E&!2zu&D?4o>r15P zp)CwQ{awhu!-LIKZy)c7uY8-1*!r*EGbkw>* zc8j2n1`@G?*Ao<;`7v>mALBN{86GH72{~oa`9`Ai@X)9tBsmmfAM##YM8vXH*jI!G zzL)O?PlhI_^9c*{a(i_?R~uqR3D@1YU5LI1lY$80=(&|C>nsV1HUGj zm+o~>RX|z_g!CH7a!9Lt=YewSx1pb%ndj$^-2MK&j+Of3i}lBWna5n!c8(%CgI2?T z_{h<=$XnrP$I0&QlReSilcBJ}?)5nc+7)e7OcvTqTYSwFG4fB2WH@FhZ8b(GJPEjwz) zJ60XVD~_rMj;fgx%Z`_z?ytJl^GVO6x_6e8Q_FSlEc#Ec6rCPF_C{?z?Z_ zI(7Zj+^he3@Xo=9uGbcJFFCsw#V&YPc;4l{F`QlB=yfpTkK5esR&K$^wG(ln#D;X$ z&URj0I3xmnpO+vH3O&Oh3hD71PkswUu$*Zqck^==zNuwX82hu_ugY(uo0{6kQvg@NK%E5umj2DNG|vdVcU7kP5Q$+snEM>sO5cYn_!gp`ZZq19WC*r6`G&3!BY!+u7kiQ8P`G-Pzgi zJ695pkb&KHraiXK`R+O2d7txrzw@25WU&|txY3(G8v0csA^(9NDQ8U*OEVlH=ZTxR zx!r`SAxAm(t)Utg@|0(xKm`ajyTx5vs&yeRzgxFUPxZSD)UeA)jk`?Lw98D*EKS&L z*=40xMvJ?1cIDDs7a7nNkuu`et|D&Tvl_Jx&nBBfZEpQQp4%{BcN+&BZqtBiAWwv} zX1&|IiX0PY{v+w;fxH38GmzJ;VVulQ=CQCm1uTzs07}L9WXXmBTaqh>aTP*s23C?$ zElDlSoy$^-+%`yMIox@V@@37db=z59X9+1G?jpAX^0D5y^MQKUmICN)wz`e(Ld<`R zqpOZ`)y{i3Mm^P>VhH%cKL4;!ma9cY*ctSP6=6?M4l8_H=(M8WHyRF(gnYw_cwlUF zSi%fcl1IjI8O!$t^t^Tj`EdcKo}6+n)Q$} zpaqI&6vhdR(m}dj9XE&*G0v@}hNHS_y^`ma=*gg8dNCL}D$!9I42AFE+EaA0nxF`y zBQ&h&rIW#c6!J@oa6$_Eu0m&xir(uD`A$e)uVVCiPmBb{hLJXVy|0e>hLb5dUhiO# z%HiQ)ND7TWGB1az0bqTlNahiUN0SXcrsng?l1 z@FPl1!)qh-xIF6fOAYc#9JU7ZLK2l5PL2(SyveWn(bLon8JLG1lh=ax*W@!x?%6$Y zqvdRO+-f_!H*T~}$dm08r_R-XEELh=R1QTL8iDraz_+3c2Tw>NV_`*0rSKRH`BQTO zi9`>-v+!HeLTov4!5o#4eoSGmH@TNm-x!uHPsgrjxH8hmrS_l)W_pXJ8}{@Nlz);t z&-aLCG6toNHh-la9K8$&`&siWND*|ICAgNiA!{2we6K;xyWDs4sS>ldEF^lwUeod% zD(x2B;ufyg@@Ps*%7nBYZB;Va2QBhw_rXdpAs(R@XCcF-azQ_GQ%SwJN-`XbwIdak zYPkkx2}%XYo9R-eJ~~<;Q{gW)D=tz26W_A{zn{SG8}PH2k_{x){s4^npvFbkLQTl~ zJinFx~o7#_JLJ{R*Ng7oID)|D6HX!*&0un@md>B*yM29oyZkow&1o0f{Qw*Jv;dp@7m{(H>C+iw>Aws-vrc z21$qfUNxCM2T=v^lbz7Xdt{s>G#1fuckQ~xwGAJvZTPuvv9a@m#?D(ptkE6$dLX)1 zT69Y1tP|RV#$c%ZO`h{Y-S_Hd_*h=WqPZesuDI)TEjp_|a8}Q*`)Tdf+Btu$rsaBP z)VXKTzGq@*Jl}btf2x00KiMD2Z(7XX6wTih%il8b;-_oMr=FX2M9g)cne*P-b8gSy z^xU--FWM?TuvPr{&_}k0c){w$f~sgiRji<9F}Eg?Tk|xh z?0fE;iLI6;L8kvo@Wf(&isGWl;udWUAKM!K=Qk$D65&M0Cpq>658;2y{{lnwI`<;S zeP=^Q6Pe@MpA&wj1?rk}XAYUSaqXLh`Mg@BpXb_Jg!xvae_2xlEH@mE_Ezym71zE^ zf1_GJx=xSr&tiEup5u_-%mM$67Upl`kf+tS(`mY%%y5!x>D>dO~pS9Q@wq3O=N193e@6%ku5t4TD+6%nPUDB^S6U8_phuC zQ@R|us#oUfsY_|gKU=?HH~z^qetK1`sPRobX9_wy4!9azT?Y>AZ)mD-v>3YpK)1Ng zthN|EaHObfJ1T|XJBisF>zgb_i;?2O3L=Q6qBR&_%i`tH;b53{=PZm5l`FkrgyLvv|7ujs;tsSV$+Uo^WSW>+S}2*hf(UOyOh?p?I+1&EPXaG`dp zcBXH#Hj-CA(H>t{HPIP&6kh0_>Yg5qIVvYQ;zetwebYzYHcSei78ZZvaK`Ng(-r5p z#*5cpYPr}lX9^&8EaZvL62IP5vqoZ>p ze^bYGHfe9_1*FZ$bJKx5H}g59i#RCrW-;?iI4I|)%h*|OxLLzvY8~UOXLJ*XC2TZy zak`t&>mc=(P}^P3-)eAl=jm?qT({PETM&^p7$E+3p0V3yy1iP5bUBuAd!4a+BY(S5 z1GV3NhC_NI542)rm>(?;a&}ntcc7wQA+Km&F{t3GfrR)5a>C%|9v-&cU>0i@Jj7%0 zXqtI94~823F(D*8hDS*9h>zhBlRVnT@POfr)ur=r05|>>;D+9#ck2*b_(0^qVH%D~ znXW$#nR>IrI*(8WFd(IhGeZ~I3nve#Mc)FD;g6_Ad_pZjR*T_}tVR2TTHax`7_k-x z$A0|(9yQ$)YKkTS7N3Ss#e^)I3(!WH4oc*o%VI=1U|@y-Ej#v_dejpo8k+p5z)}wF zDBp+Q(s#gX<<$k;pXN9KOZI^F+2jJr#8V;8!yPGw!QdG7EI%M-T}o&eWzNm@Y1|qQ z-^c;7PUCDBoY4{O^i;E^3O!)p0zE|AAZhO3>^xVFQ7vpC8TRrD` zJ}j=A8@%2eIUI}}rxC9lEe?NV9!rSCTzsD=InRH=RPtF{8{vMywF>8JfXF^e<4|pu zS<=8P36Gnl<;&?n_6@&$mbgYZ8)liI^=yXt9x#~7Lv$}R<|@x@3=~klYR|kXs*{Bn zT!k(BkwTm%V-+q}dfc@zpiqud@x);VF05b)WtKYXh2=9lXDjA(vBJ%Zw#^gTPffOE zgDqXJh`e$*TI~DCd<2XH<~$hYye(fWq1E-QtrbRjn{d9W^)G#p2VocBAX5)|a8S%T zguwL_LkL`MEaeb-FN61&y}$(d#1lJ@`xe=rV&5VgNN*xP?Fz!}>h(p7k9=hIFIz!u zg zb$fIpD{c=(#AKO%4I>Vs_jUAqn8b}ztWw6yxx=o*Ds11OiZ4gD?L&_P=T6MB355BV z47j_>#yjFwH2~EGK|IO5)1Dw0&g}f0h4-1CF!Q3HduL;UU^vtKIScPIKVf`)T7Z?w zivd<9hL|tFEg6ZmXo8EI%#mDI%;bujZD_T7r*P4^;REM}s|V-4_U_9u=jM;iTjCb$ zWZ9bsGdW(GYkT)l%(>}f^X8}I_}fRySXRq@8#L(m4x+c*wdTJSdLuL~MXVKz`ih9Y zB5ukX?*VyXsQg04Y5Z<`Lx+XCvAN9*!Ggebn1lsU2ecm&(+)urYRRQvfp`9iHyCa% z1(|x&ayMPpvb&L9R8|?z&9fT|jfR9se;N?<;?l@)01Zg09vSatGwumiwh|moTGqi{ ze#WGpi|f(ue8$W!vI0lykI7*D=I9x5EIe4Z8SV-7><|nJv>NX5pdpGl7#xPo$f@5qWKaKxWBcLFD>3pS}U;GZXe>p{`l!7S4?3n-dLJPq`Z zfEK`*i2cZL52qPW_VSXGd@Zj5Iz|IuC4i{xC1@JcS3B$iD_bpTBPkds#jNc}gRPKm z*;N)SE{`q+4H&WoJPjsaA=Ql)u)l5@M|uIK@{ot^1rsNjI<;<*;d2W^%{ZnRE@aqu z47jw6%LiQ9`=EPh>g;{YwHp{y`Cf#I87|dNAD9=jb7UPk!^8342e2q)@|d#mc^(=+ z4g2^`ZZE)Ak0DiO0M-x06+xUnn*r8iNNv-M9AN(OXtJ4QS6D6SR;-Y2a+_1;;uSb7 zZtD{`avIVzoPAY8EGond2PW^_`m_n$h2t@!o_2tGH+k;SRFKl;>o1E?14AE(OuX8I zeDiRbnG#jD9(Gnx#1p=-AEZtA^r)ohK=Qz?Sx7OfH)3!*c2qj0m{sdm@=Kevb)j{P z9UBx)FaURT(n$&K+)gV(Ff=%#m;#dQhkHGC??)6Pl?Ek>&ktlcdwdJCj-&TFdf!3s zt0$LgGu@GjRWuZu<{vpR3L;W5c6avd>+NW7b9X2ryM3g%e(84f@VtYjMj!PLsfYsC zJ`{EmQ$c0;*dSg-!jlJj8WY45I0p2u7_D~PntC(gJ?R@BlN3{OAn;`b z>$;2^RK*vnuS({8HmFQAOVoEH2nNg`9JHo@`BnJIqac^SHdwiSyz8!Y)r97*qX3L` zAcjw!J2h>H<&@1FxpM6Cu~<&yC%Hv&N703@sjlgRGe=^MYOvTjoXhQZ1;rPFQ^6Sn z+yzc_erhlJi4O84G`z)mw`kQR;}4B9oBsZVpYFZ7H&)a#DT0}-u4$qtp5vId#&Xuh zOUthmUoM`q#%%@D^)Xvjyrle6@M3UQKUWni*}7QL7AW zi|a4dUaXx7&gFf$s`+~Fq%K}sg{92)$4a+MS$~~d^q)Cv&V6&H^2&zG8)m<`Sl0Ss zS!+DE=t9m^&SxcMmxeA5&4k|$#Ve|=bY1S6efe5JtYSxeeeIP~mru=EuF=@~7vk$` zt_)osn*GML-q^Yw@$$+mEtgy3uCgn-%er{QGxu}!`30ZzMqA#4CX!!>$HL-77e~yF z@qK@Iu#q?lE^MCK4A+(x=N%Xf9>OnWd{i+1Y~_p1!u!pRPK)lQnFAjlg{Y5p6px{9_c?>S=#kwnJV77iZzO^d)%Zu%`gHz&xt1kLxRk2Oh#Bhkl_Sbs}?k$U_$r zK{skW0@#<}oCZ2ZmmStTlpG;_e;V3&bZ#C|P~Yn-9$^jr2q~EBJD-wIAnCQ)9(1J3 zd&4c%r*$LPqyR#*>nkm@j0}Rwp`IOR#hz*ngW&WBPz7a(9B<*l&cH6?S;QL{p`|F1 z7&1|JGs0}P3)tSQHqq~41X@%c7O0p)QW!*yYJ*@~mWMYx56f2g!BLrF1q`4p+kNWh zv(T}Sii+?B5xoeEKSbtlgNMleftfffXAWLF7%h5bG4GY}-FFT7Zx>&s}p+M8`f(J5g#D5%aLF)0tlzy@XLI#(c zq6JX~GERMug{y@k}ls*{XE&t80~&#~Bx3;j0rnOQ5n45_$(@ z7R*U3xURotpSL`~ghbgYE_VhFaS+VPEV$-j!Sz#-{hr86ha&yn$YFm(8j2hqiuMO1 zF9oCfk3~C=Kfo-BmP*b(19dWdkBcWF#!B?XJ8uiK#Fhin`BL&WQ;i~(F94;qru}zkrJCxvo{>+p!$)DSj5P^oV hvf2|mr1T^&KY_fk9JR)YiqtE)sObE?38LBD{4WytUzPv> literal 0 HcmV?d00001 diff --git a/svscan/scanners/network_scanner.py b/svscan/scanners/network_scanner.py new file mode 100644 index 0000000..2a43e8c --- /dev/null +++ b/svscan/scanners/network_scanner.py @@ -0,0 +1,113 @@ +import asyncio +import socket +from dataclasses import dataclass +from ipaddress import ip_network, ip_address +from typing import Iterable, List, Dict, Any + + +DEFAULT_COMMON_PORTS = [22, 80, 443] + + +@dataclass +class HostScanResult: + host: str + open_ports: List[int] + + +def parse_ports(ports_arg: str) -> List[int]: + """Parse comma-separated ports into a sorted unique list of ints.""" + ports: List[int] = [] + for part in ports_arg.split(","): + part = part.strip() + if not part: + continue + if "-" in part: + start_str, end_str = part.split("-", 1) + start = int(start_str) + end = int(end_str) + ports.extend(range(start, end + 1)) + else: + ports.append(int(part)) + return sorted(set(ports)) + + +def expand_targets(targets: Iterable[str]) -> List[str]: + """Expand IPs/hostnames/CIDRs into a list of IP strings. + + Hostnames are resolved to a single IP; CIDRs are expanded (IPv4 only by default). + """ + result: List[str] = [] + for t in targets: + t = t.strip() + if not t: + continue + try: + # Try CIDR expansion + if "/" in t: + net = ip_network(t, strict=False) + for ip in net.hosts(): + result.append(str(ip)) + continue + # Try IP literal + _ = ip_address(t) + result.append(t) + except ValueError: + # Resolve hostname + try: + resolved = socket.gethostbyname(t) + result.append(resolved) + except socket.gaierror: + # Skip unresolved + pass + # De-duplicate preserving order + seen = set() + unique: List[str] = [] + for ip in result: + if ip in seen: + continue + seen.add(ip) + unique.append(ip) + return unique + + +async def _probe_port(host: str, port: int, timeout: float) -> bool: + loop = asyncio.get_running_loop() + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.setblocking(False) + await asyncio.wait_for(loop.sock_connect(sock, (host, port)), timeout=timeout) + return True + except Exception: + return False + + +async def _scan_host(host: str, ports: List[int], timeout: float, sem: asyncio.Semaphore) -> HostScanResult: + open_ports: List[int] = [] + + async def probe(p: int) -> None: + async with sem: + is_open = await _probe_port(host, p, timeout) + if is_open: + open_ports.append(p) + + await asyncio.gather(*(probe(p) for p in ports)) + open_ports.sort() + return HostScanResult(host=host, open_ports=open_ports) + + +async def scan_targets( + targets: Iterable[str], + ports: List[int] = None, + timeout: float = 0.5, + concurrency: int = 200, +) -> List[Dict[str, Any]]: + """Scan targets for open TCP ports using async connect. + + Returns a list of dicts for easy JSON serialization. + """ + if ports is None: + ports = DEFAULT_COMMON_PORTS + ips = expand_targets(targets) + sem = asyncio.Semaphore(concurrency) + results = await asyncio.gather(*(_scan_host(ip, ports, timeout, sem) for ip in ips)) + return [{"host": r.host, "open_ports": r.open_ports} for r in results] \ No newline at end of file diff --git a/svscan/scanners/vuln_scanner.py b/svscan/scanners/vuln_scanner.py new file mode 100644 index 0000000..4ea58f5 --- /dev/null +++ b/svscan/scanners/vuln_scanner.py @@ -0,0 +1,169 @@ +import asyncio +import json +import re +import socket +import contextlib +from dataclasses import dataclass +from typing import Dict, List, Any, Optional, Tuple + +from importlib import resources + +from svscan.scanners.network_scanner import scan_targets + + +@dataclass +class ServiceFingerprint: + service: str + port: int + evidence: str + meta: Dict[str, Any] + + +async def _read_banner(host: str, port: int, timeout: float) -> Optional[str]: + try: + reader, writer = await asyncio.wait_for(asyncio.open_connection(host, port), timeout=timeout) + try: + # Give the service a moment to speak first (SSH/FTP often do) + await asyncio.sleep(0.1) + data = await asyncio.wait_for(reader.read(256), timeout=timeout) + banner = data.decode(errors="ignore").strip() + return banner if banner else None + finally: + writer.close() + with contextlib.suppress(Exception): + await writer.wait_closed() + except Exception: + return None + + +async def _http_server_header(host: str, port: int, timeout: float) -> Optional[Tuple[str, Dict[str, str]]]: + request = f"GET / HTTP/1.0\r\nHost: {host}\r\nUser-Agent: svscan/0.1\r\n\r\n".encode() + try: + reader, writer = await asyncio.wait_for(asyncio.open_connection(host, port), timeout=timeout) + try: + writer.write(request) + await writer.drain() + data = await asyncio.wait_for(reader.read(2048), timeout=timeout) + raw = data.decode(errors="ignore") + headers: Dict[str, str] = {} + lines = raw.split("\r\n") + for line in lines[1:]: + if not line: + break + if ":" in line: + k, v = line.split(":", 1) + headers[k.strip().lower()] = v.strip() + server = headers.get("server") + if server: + return server, headers + finally: + writer.close() + with contextlib.suppress(Exception): + await writer.wait_closed() + except Exception: + return None + return None + + +async def fingerprint_services(host: str, open_ports: List[int], timeout: float) -> List[ServiceFingerprint]: + tasks: List[asyncio.Task] = [] + results: List[ServiceFingerprint] = [] + + async def fp_http(port: int) -> None: + res = await _http_server_header(host, port, timeout) + if res is not None: + server, headers = res + results.append(ServiceFingerprint(service="http", port=port, evidence=server, meta={"headers": headers})) + + async def fp_ssh(port: int) -> None: + banner = await _read_banner(host, port, timeout) + if banner: + results.append(ServiceFingerprint(service="ssh", port=port, evidence=banner, meta={})) + + async def fp_ftp(port: int) -> None: + banner = await _read_banner(host, port, timeout) + if banner: + results.append(ServiceFingerprint(service="ftp", port=port, evidence=banner, meta={})) + + async def fp_smtp(port: int) -> None: + banner = await _read_banner(host, port, timeout) + if banner: + results.append(ServiceFingerprint(service="smtp", port=port, evidence=banner, meta={})) + + for p in open_ports: + if p in (80, 8080, 8000): + tasks.append(asyncio.create_task(fp_http(p))) + elif p == 22: + tasks.append(asyncio.create_task(fp_ssh(p))) + elif p == 21: + tasks.append(asyncio.create_task(fp_ftp(p))) + elif p == 25: + tasks.append(asyncio.create_task(fp_smtp(p))) + + if tasks: + await asyncio.gather(*tasks) + return results + + +def load_vuln_db() -> List[Dict[str, Any]]: + with resources.files("svscan.data").joinpath("vuln_db.json").open("r", encoding="utf-8") as f: + return json.load(f) + + +def match_vulnerabilities(fingerprints: List[ServiceFingerprint], db: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + findings: List[Dict[str, Any]] = [] + for fp in fingerprints: + for rule in db: + if rule.get("service") != fp.service: + continue + match = rule.get("match", {}) + mtype = match.get("type") + pattern = match.get("pattern") + if not mtype or not pattern: + continue + try: + regex = re.compile(pattern, re.IGNORECASE) + except re.error: + continue + + evidence_value: Optional[str] = None + if mtype == "banner_regex": + evidence_value = fp.evidence + elif mtype == "header_regex": + header_name = match.get("header", "server").lower() + headers = fp.meta.get("headers", {}) if fp.meta else {} + evidence_value = headers.get(header_name) + else: + continue + + if evidence_value and regex.search(evidence_value): + findings.append( + { + "id": rule.get("id"), + "severity": rule.get("severity", "info"), + "description": rule.get("description", ""), + "references": rule.get("references", []), + "service": fp.service, + "port": fp.port, + "evidence": evidence_value, + } + ) + return findings + + +async def run_vuln_scan( + targets: List[str], + ports: List[int], + timeout: float = 0.8, + concurrency: int = 200, +) -> List[Dict[str, Any]]: + net_results = await scan_targets(targets, ports, timeout=timeout, concurrency=concurrency) + db = load_vuln_db() + + out: List[Dict[str, Any]] = [] + for host in net_results: + open_ports = host.get("open_ports", []) + fps = await fingerprint_services(host["host"], open_ports, timeout) + findings = match_vulnerabilities(fps, db) + out.append({"host": host["host"], "open_ports": open_ports, "findings": findings}) + return out \ No newline at end of file diff --git a/svscan/utils/__init__.py b/svscan/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/svscan/utils/targets.py b/svscan/utils/targets.py new file mode 100644 index 0000000..9418113 --- /dev/null +++ b/svscan/utils/targets.py @@ -0,0 +1,7 @@ +from typing import List + +from svscan.scanners.network_scanner import expand_targets as expand_targets_from_scanner + + +def expand_targets(targets: List[str]) -> List[str]: + return expand_targets_from_scanner(targets) \ No newline at end of file diff --git a/svscan/version.py b/svscan/version.py new file mode 100644 index 0000000..58c8455 --- /dev/null +++ b/svscan/version.py @@ -0,0 +1 @@ +VERSION = "0.1.0" \ No newline at end of file From f9b76c3837e04cc33fb0854f667d7a240a0931b8 Mon Sep 17 00:00:00 2001 From: egorTriceps <92254080+egorTriceps@users.noreply.github.com> Date: Thu, 14 Aug 2025 17:33:16 +0600 Subject: [PATCH 2/3] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3ff22e2..b9a2758 100644 --- a/README.md +++ b/README.md @@ -70,4 +70,4 @@ svscan vuln scan example.com -p 21,22,25,80,443 -o report.json - Cloud SaaS‑версия: сканы через управляемую облачную очередь/агенты ## Дисклеймер -Используйте инструмент только на ресурсах, на тест‑стендах или при наличии письменного разрешения. Ответственность за использование лежит на пользователе. +Используйте инструмент только на ресурсах, на тест‑стендах или при наличии письменного разрешения. Ответственность не лежит на egorTriceps. Ответственность за использование лежит только на пользователе. From ba8fd5856179930608805d566d6c9800d61dc388 Mon Sep 17 00:00:00 2001 From: egorTriceps <92254080+egorTriceps@users.noreply.github.com> Date: Thu, 14 Aug 2025 17:34:14 +0600 Subject: [PATCH 3/3] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b9a2758..9729d26 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# SVScan — модульный сканер сети и уязвимостей +# NetNinjaScan — модульный сканер сети и уязвимостей Небольшой консольный аналог Nessus/OpenVAS/NetScanPro: быстрый сетевой сканер (TCP connect), лёгкое фингерпринтинг‑сканирование и локальная база сигнатур уязвимостей.