mirror of
https://github.com/bee-san/RustScan.git
synced 2026-11-07 17:02:04 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2288e2c29f |
No files matched your search
@@ -7,10 +7,6 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
IMPORTANT: If you do not fill this out, your issue will be automatically closed.
|
||||
|
||||
As of November 2024, RustScan no longer offiically supports any packages other than `cargo`. If you have an issue with Docker or a `.deb`, please bring it up with whoever created it :)
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: cargo
|
||||
directory: "/"
|
||||
schedule:
|
||||
|
||||
+78
-165
@@ -1,179 +1,92 @@
|
||||
name: CD Pipeline
|
||||
|
||||
on: [push]
|
||||
name: Build
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
schedule:
|
||||
- cron: '00 01 * * *'
|
||||
|
||||
jobs:
|
||||
build-nix:
|
||||
env:
|
||||
IN_PIPELINE: true
|
||||
runs-on: ${{ matrix.os }}
|
||||
if: github.ref == 'refs/heads/master'
|
||||
strategy:
|
||||
matrix:
|
||||
type: [ubuntu-x64, ubuntu-x86, armv7, aarch64]
|
||||
include:
|
||||
- type: ubuntu-x64
|
||||
os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-musl
|
||||
name: x86_64-linux-rustscan
|
||||
path: target/x86_64-unknown-linux-musl/release/rustscan
|
||||
pkg_config_path: /usr/lib/x86_64-linux-gnu/pkgconfig
|
||||
- type: ubuntu-x86
|
||||
os: ubuntu-latest
|
||||
target: i686-unknown-linux-musl
|
||||
name: x86-linux-rustscan
|
||||
path: target/i686-unknown-linux-musl/release/rustscan
|
||||
pkg_config_path: /usr/lib/i686-linux-gnu/pkgconfig
|
||||
- type: armv7
|
||||
os: ubuntu-latest
|
||||
target: armv7-unknown-linux-gnueabihf
|
||||
name: armv7-linux-rustscan
|
||||
path: target/armv7-unknown-linux-gnueabihf/release/rustscan
|
||||
pkg_config_path: /usr/lib/x86_64-linux-gnu/pkgconfig
|
||||
- type: aarch64
|
||||
os: ubuntu-latest
|
||||
target: aarch64-unknown-linux-gnu
|
||||
name: aarch64-linux-rustscan
|
||||
path: target/aarch64-unknown-linux-gnu/release/rustscan
|
||||
pkg_config_path: /usr/lib/x86_64-linux-gnu/pkgconfig
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Cache cargo & target directories
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Build binary
|
||||
uses: houseabsolute/actions-rust-cross@v1
|
||||
with:
|
||||
command: build
|
||||
target: ${{ matrix.target }}
|
||||
args: "--locked --release"
|
||||
strip: true
|
||||
toolchain: stable
|
||||
- name: Build tar.gz for homebrew installs
|
||||
if: matrix.type == 'ubuntu-x64'
|
||||
run: |
|
||||
tar czf ${{ matrix.name }}.tar.gz -C target/x86_64-unknown-linux-musl/release rustscan
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ matrix.name }}
|
||||
path: ${{ matrix.path }}
|
||||
- uses: actions/upload-artifact@v7
|
||||
if: matrix.type == 'ubuntu-x64'
|
||||
with:
|
||||
name: ${{ matrix.name }}.tar.gz
|
||||
path: ${{ matrix.name }}.tar.gz
|
||||
build-deb:
|
||||
needs: [build-nix]
|
||||
check:
|
||||
name: Check
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
IN_PIPELINE: true
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Install cargo-deb
|
||||
run: cargo install -f cargo-deb
|
||||
- uses: awalsh128/cache-apt-pkgs-action@v1
|
||||
with:
|
||||
packages: musl-tools # provides musl-gcc
|
||||
version: 1.0
|
||||
- name: Install musl toolchain
|
||||
run: rustup target add x86_64-unknown-linux-musl
|
||||
- name: Deb Build
|
||||
run: cargo deb --target=x86_64-unknown-linux-musl
|
||||
- name: Upload Deb Artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: rustscan.deb
|
||||
path: ./target/x86_64-unknown-linux-musl/debian/*
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v2
|
||||
|
||||
build-macos:
|
||||
env:
|
||||
IN_PIPELINE: true
|
||||
runs-on: macos-latest
|
||||
if: github.ref == 'refs/heads/master'
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Cache cargo & target directories
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Build binary
|
||||
uses: houseabsolute/actions-rust-cross@v1
|
||||
- name: Install stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
command: build
|
||||
target: x86_64-apple-darwin
|
||||
args: "--locked --release"
|
||||
strip: true
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
- name: Build tar.gz for homebrew installs
|
||||
run: |
|
||||
tar czf x86_64-macos-rustscan.tar.gz -C target/x86_64-apple-darwin/release rustscan
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: x86_64-macos-rustscan
|
||||
path: target/x86_64-apple-darwin/release/rustscan
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: x86_64-macos-rustscan.tar.gz
|
||||
path: x86_64-macos-rustscan.tar.gz
|
||||
|
||||
build-macos-aarch64:
|
||||
env:
|
||||
IN_PIPELINE: true
|
||||
runs-on: macos-latest
|
||||
if: github.ref == 'refs/heads/master'
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Cache cargo & target directories
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Build binary
|
||||
uses: houseabsolute/actions-rust-cross@v1
|
||||
with:
|
||||
command: build
|
||||
target: aarch64-apple-darwin
|
||||
args: "--locked --release"
|
||||
strip: true
|
||||
toolchain: stable
|
||||
- name: Build tar.gz for homebrew installs
|
||||
run: |
|
||||
tar czf aarch64-macos-rustscan.tar.gz -C target/aarch64-apple-darwin/release rustscan
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: aarch64-macos-rustscan
|
||||
path: target/aarch64-apple-darwin/release/rustscan
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: aarch64-macos-rustscan.tar.gz
|
||||
path: aarch64-macos-rustscan.tar.gz
|
||||
override: true
|
||||
|
||||
build-windows:
|
||||
env:
|
||||
IN_PIPELINE: true
|
||||
runs-on: ${{ matrix.os }}
|
||||
if: github.ref == 'refs/heads/master'
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
|
||||
- name: Run cargo check
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
|
||||
test:
|
||||
name: Test Suite
|
||||
strategy:
|
||||
matrix:
|
||||
type: [windows-x64, windows-x86]
|
||||
include:
|
||||
- type: windows-x64
|
||||
os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
name: x86_64-windows-rustscan.exe
|
||||
path: target\x86_64-pc-windows-msvc\release\rustscan.exe
|
||||
- type: windows-x86
|
||||
os: windows-latest
|
||||
target: i686-pc-windows-msvc
|
||||
name: x86-windows-rustscan.exe
|
||||
path: target\i686-pc-windows-msvc\release\rustscan.exe
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
rust: [stable]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Cache cargo & target directories
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Build binary
|
||||
uses: houseabsolute/actions-rust-cross@v1
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
command: build
|
||||
target: ${{ matrix.target }}
|
||||
args: "--locked --release"
|
||||
strip: true
|
||||
profile: minimal
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
|
||||
- name: Run cargo test
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
|
||||
lints:
|
||||
name: Lints
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Install stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
- uses: actions/upload-artifact@v7
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
|
||||
- name: Run cargo fmt
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
name: ${{ matrix.name }}
|
||||
path: ${{ matrix.path }}
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
- name: Run cargo clippy
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: -- -D warnings
|
||||
|
||||
- name: Run rustdoc lints
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: doc
|
||||
args: --workspace --all-features --no-deps --document-private-items
|
||||
@@ -1,14 +0,0 @@
|
||||
name: Dependabot Auto-Merge
|
||||
|
||||
jobs:
|
||||
dependabot:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.actor == 'dependabot[bot]' }}
|
||||
steps:
|
||||
- name: Enable auto-merge for Dependabot PRs
|
||||
run: |
|
||||
gh pr review --approve "$PR_URL"
|
||||
gh pr merge --auto --merge "$PR_URL"
|
||||
env:
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,46 @@
|
||||
name: Publish Docker image
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
release:
|
||||
types: [published]
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'Dockerfile'
|
||||
- 'Cargo.toml'
|
||||
- 'Cargo.lock'
|
||||
- 'contributing.Dockerfile'
|
||||
- '.github/workflows/docker.yml'
|
||||
|
||||
jobs:
|
||||
push_to_registry:
|
||||
name: Push Docker image to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: rustscan/rustscan
|
||||
flavor: latest=true
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
push: ${{ contains(fromJson('["push", "release"]'), github.event_name) }} # Publish to docker registry only on push event or new release.
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -0,0 +1,19 @@
|
||||
name: Check Markdown links
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
schedule:
|
||||
# Run everyday at 9:00 AM (See https://pubs.opengroup.org/onlinepubs/9699919799/utilities/crontab.html#tag_20_25_07)
|
||||
- cron: "0 9 * * *"
|
||||
|
||||
jobs:
|
||||
markdown-link-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: gaurav-nelson/github-action-markdown-link-check@v1
|
||||
with:
|
||||
use-quiet-mode: 'yes'
|
||||
config-file: './.github/workflows/mlc_config.json'
|
||||
@@ -0,0 +1,230 @@
|
||||
name: Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'Cargo.toml'
|
||||
- 'Cargo.lock'
|
||||
- '.github/workflows/release.yml'
|
||||
|
||||
env:
|
||||
BIN_NAME: rustscan
|
||||
PROJECT_NAME: rustscan
|
||||
REPO_NAME: RustScan/RustScan
|
||||
# BREW_TAP: RustScan/homebrew-tap
|
||||
|
||||
jobs:
|
||||
dist:
|
||||
name: Dist
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false # don't fail other jobs if one fails
|
||||
matrix:
|
||||
build: [x86_64-linux, x86_64-macos, x86_64-windows] #, x86_64-win-gnu, win32-msvc
|
||||
include:
|
||||
- build: x86_64-linux
|
||||
os: ubuntu-20.04
|
||||
rust: stable
|
||||
target: x86_64-unknown-linux-gnu
|
||||
cross: false
|
||||
- build: x86_64-macos
|
||||
os: macos-latest
|
||||
rust: stable
|
||||
target: x86_64-apple-darwin
|
||||
cross: false
|
||||
- build: x86_64-windows
|
||||
os: windows-2019
|
||||
rust: stable
|
||||
target: x86_64-pc-windows-msvc
|
||||
cross: false
|
||||
# - build: aarch64-linux
|
||||
# os: ubuntu-20.04
|
||||
# rust: stable
|
||||
# target: aarch64-unknown-linux-gnu
|
||||
# - build: aarch64-macos
|
||||
# os: macos-latest
|
||||
# rust: stable
|
||||
# target: aarch64-apple-darwin
|
||||
# - build: x86_64-win-gnu
|
||||
# os: windows-2019
|
||||
# rust: stable-x86_64-gnu
|
||||
# target: x86_64-pc-windows-gnu
|
||||
# - build: win32-msvc
|
||||
# os: windows-2019
|
||||
# rust: stable
|
||||
# target: i686-pc-windows-msvc
|
||||
|
||||
steps:
|
||||
- name: Strip release binary (linux and macos)
|
||||
if: matrix.build == 'x86_64-windows' # Windows is missing some dependencies
|
||||
run: |
|
||||
git config --global pack.windowMemory "100m"
|
||||
git config --global pack.packSizeLimit "100m"
|
||||
git config --global pack.threads "1"
|
||||
git config --global pack.deltaCacheSize "512m"
|
||||
|
||||
- name: Install python 3.10 for windows
|
||||
if: matrix.build == 'x86_64-windows' # Windows is missing some dependencies
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Install ${{ matrix.rust }} toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: ${{ matrix.rust }}
|
||||
target: ${{ matrix.target }}
|
||||
override: true
|
||||
|
||||
- name: Run cargo test
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
use-cross: ${{ matrix.cross }}
|
||||
command: test
|
||||
args: --release --locked --target ${{ matrix.target }}
|
||||
|
||||
- name: Build release binary
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
use-cross: ${{ matrix.cross }}
|
||||
command: build
|
||||
args: --release --locked --target ${{ matrix.target }}
|
||||
|
||||
- name: Strip release binary (linux and macos)
|
||||
if: matrix.build == 'x86_64-linux' || matrix.build == 'x86_64-macos'
|
||||
run: strip "target/${{ matrix.target }}/release/$BIN_NAME"
|
||||
|
||||
- name: Strip release binary (arm)
|
||||
if: matrix.build == 'aarch64-linux'
|
||||
run: |
|
||||
docker run --rm -v \
|
||||
"$PWD/target:/target:Z" \
|
||||
rustembedded/cross:${{ matrix.target }} \
|
||||
aarch64-linux-gnu-strip \
|
||||
/target/${{ matrix.target }}/release/$BIN_NAME
|
||||
- name: Build archive
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir dist
|
||||
if [ "${{ matrix.os }}" = "windows-2019" ]; then
|
||||
cp "target/${{ matrix.target }}/release/$BIN_NAME.exe" "dist/"
|
||||
else
|
||||
cp "target/${{ matrix.target }}/release/$BIN_NAME" "dist/"
|
||||
fi
|
||||
- uses: actions/[email protected]
|
||||
with:
|
||||
name: bins-${{ matrix.build }}
|
||||
path: dist
|
||||
|
||||
publish:
|
||||
name: Publish
|
||||
needs: [dist]
|
||||
if: contains(fromJson('["push", "schedule"]'), github.event_name) && startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: false
|
||||
|
||||
- uses: actions/download-artifact@v2
|
||||
# with:
|
||||
# path: dist
|
||||
# - run: ls -al ./dist
|
||||
- run: ls -al bins-*
|
||||
|
||||
- name: Calculate tag name
|
||||
run: |
|
||||
name=dev
|
||||
if [[ $GITHUB_REF == refs/tags/v* ]]; then
|
||||
name=${GITHUB_REF:10}
|
||||
fi
|
||||
echo ::set-output name=val::$name
|
||||
echo TAG=$name >> $GITHUB_ENV
|
||||
id: tagname
|
||||
|
||||
- name: Build archive
|
||||
shell: bash
|
||||
run: |
|
||||
set -ex
|
||||
rm -rf tmp
|
||||
mkdir tmp
|
||||
mkdir dist
|
||||
for dir in bins-* ; do
|
||||
platform=${dir#"bins-"}
|
||||
unset exe
|
||||
if [[ $platform =~ "windows" ]]; then
|
||||
exe=".exe"
|
||||
fi
|
||||
pkgname=$PROJECT_NAME-$TAG-$platform
|
||||
mkdir tmp/$pkgname
|
||||
# cp LICENSE README.md tmp/$pkgname
|
||||
mv bins-$platform/$BIN_NAME$exe tmp/$pkgname
|
||||
chmod +x tmp/$pkgname/$BIN_NAME$exe
|
||||
if [ "$exe" = "" ]; then
|
||||
tar cJf dist/$pkgname.tar.xz -C tmp $pkgname
|
||||
else
|
||||
(cd tmp && 7z a -r ../dist/$pkgname.zip $pkgname)
|
||||
fi
|
||||
done
|
||||
- name: Upload binaries to release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: dist/*
|
||||
file_glob: true
|
||||
tag: ${{ steps.tagname.outputs.val }}
|
||||
overwrite: true
|
||||
|
||||
- name: Extract version
|
||||
id: extract-version
|
||||
run: |
|
||||
printf "::set-output name=%s::%s\n" tag-name "${GITHUB_REF#refs/tags/}"
|
||||
- uses: mislav/bump-homebrew-formula-action@v1
|
||||
with:
|
||||
formula-path: ${{env.PROJECT_NAME}}.rb
|
||||
homebrew-tap: ${{ env.BREW_TAP }}
|
||||
download-url: 'https://github.com/${{ env.REPO_NAME }}/releases/download/${{ steps.extract-version.outputs.tag-name }}/${{env.PROJECT_NAME}}-${{ steps.extract-version.outputs.tag-name }}-x86_64-macos.tar.xz'
|
||||
commit-message: updating formula for ${{ env.PROJECT_NAME }}
|
||||
env:
|
||||
COMMITTER_TOKEN: ${{ secrets.COMMITTER_TOKEN }}
|
||||
#
|
||||
# you can use this initial file in your homebrew-tap if you don't have an initial formula:
|
||||
# <projectname>.rb
|
||||
#
|
||||
# class <Projectname capitalized> < Formula
|
||||
# desc "A test formula"
|
||||
# homepage "http://www.example.com"
|
||||
# url "-----"
|
||||
# version "-----"
|
||||
# sha256 "-----"
|
||||
|
||||
# def install
|
||||
# bin.install "<bin-name>"
|
||||
# end
|
||||
# end
|
||||
|
||||
# Uncomment this section if you want to release your package to crates.io
|
||||
# Before publishing, make sure you have filled out the following fields:
|
||||
# license or license-file, description, homepage, documentation, repository, readme.
|
||||
# Read more: https://doc.rust-lang.org/cargo/reference/publishing.html
|
||||
|
||||
- name: Install ${{ matrix.rust }} toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: ${{ matrix.rust }}
|
||||
target: ${{ matrix.target }}
|
||||
- run: cargo publish --token ${CRATES_TOKEN}
|
||||
env:
|
||||
CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }}
|
||||
@@ -1,34 +0,0 @@
|
||||
name: Test
|
||||
on:
|
||||
pull_request:
|
||||
branches: [master]
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
test:
|
||||
name: Test Suite
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
rust: [stable]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Install stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
|
||||
- uses: taiki-e/install-action@nextest
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Install Just
|
||||
uses: extractions/setup-just@v4
|
||||
|
||||
- name: Run just
|
||||
run: just test
|
||||
@@ -1,8 +1,6 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
# generated file by build.rs for mapping nmap-payloads to rust
|
||||
src/generated.rs
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
@@ -122,5 +120,3 @@ fabric.properties
|
||||
.idea/*
|
||||
.idea
|
||||
.idea*
|
||||
|
||||
settings.json
|
||||
@@ -17,6 +17,7 @@ jobs:
|
||||
script:
|
||||
- cargo build
|
||||
- cargo test
|
||||
- "ulimit -n 5000; cargo test timelimits:: -- --ignored --test-threads=1 --show-output"
|
||||
- cargo fmt -- --check
|
||||
after_failure:
|
||||
- wget https://raw.githubusercontent.com/DiscordHooks/travis-ci-discord-webhook/master/send.sh
|
||||
|
||||
Generated
+644
-1783
File diff suppressed because it is too large.
Load diff
+21
-30
@@ -1,15 +1,15 @@
|
||||
[package]
|
||||
name = "rustscan"
|
||||
version = "2.4.1"
|
||||
version = "2.1.1"
|
||||
authors = ["Autumn <[email protected]>"]
|
||||
edition = "2018"
|
||||
description = "Faster Nmap Scanning with Rust"
|
||||
homepage = "https://github.com/rustscan/rustscan"
|
||||
repository = "https://github.com/rustscan/rustscan"
|
||||
license = "GPL-3.0-only"
|
||||
license = "MIT"
|
||||
keywords = ["port", "scanning", "nmap"]
|
||||
categories = ["command-line-utilities"]
|
||||
readme = "README.md"
|
||||
readme="README.md"
|
||||
exclude = [
|
||||
".github/*",
|
||||
"pictures/*",
|
||||
@@ -17,32 +17,31 @@ exclude = [
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.6.1", features = ["derive", "wrap_help"] }
|
||||
colored = "3.1.1"
|
||||
async-std = "1.13.2"
|
||||
colored = "2.0.0"
|
||||
structopt = "0.3.20"
|
||||
async-std = "1.7.0"
|
||||
futures = "0.3"
|
||||
rlimit = "0.11.0"
|
||||
log = "0.4.32"
|
||||
env_logger = "0.11.10"
|
||||
anstream = "=1.0.0"
|
||||
dirs = "7.0.0"
|
||||
rlimit = "0.8.3"
|
||||
shell-words = "1.0.0"
|
||||
log = "0.4.0"
|
||||
env_logger = "0.8.2"
|
||||
dirs = "3.0.1"
|
||||
gcd = "2.0.1"
|
||||
rand = "0.10.1"
|
||||
colorful = "0.3.2"
|
||||
rand = "0.7.3"
|
||||
colorful = "0.2.1"
|
||||
ansi_term = "0.12.1"
|
||||
toml = "1.1.2"
|
||||
toml = "0.5.7"
|
||||
serde = "1.0.124"
|
||||
serde_derive = "1.0.116"
|
||||
cidr-utils = "0.7.1"
|
||||
itertools = "0.15.0"
|
||||
hickory-resolver = { version = "0.24.3", features = ["dns-over-rustls"] }
|
||||
cidr-utils = "0.5.1"
|
||||
itertools = "0.9.0"
|
||||
trust-dns-resolver = { version = "0.21.1", features = ["dns-over-rustls"] }
|
||||
anyhow = "1.0.40"
|
||||
text_placeholder = { version = "0.5", features = ["struct_context"] }
|
||||
once_cell = "1.21.4"
|
||||
subprocess = "0.2.6"
|
||||
text_placeholder = { version = "0.4", features = ["struct_context"] }
|
||||
|
||||
[dev-dependencies]
|
||||
parameterized = "2.0.0"
|
||||
criterion = { version = "0.8", features = ["html_reports"] }
|
||||
wait-timeout = "0.2"
|
||||
|
||||
[package.metadata.deb]
|
||||
depends = "$auto, nmap"
|
||||
@@ -51,16 +50,8 @@ section = "rust"
|
||||
[profile.release]
|
||||
lto = true
|
||||
panic = 'abort'
|
||||
strip = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[[bin]]
|
||||
name = "rustscan"
|
||||
path = "src/main.rs"
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(tarpaulin_include)"] }
|
||||
|
||||
[[bench]]
|
||||
name = "benchmark_helpers"
|
||||
harness = false
|
||||
path = "src/main.rs"
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
FROM rust:alpine as builder
|
||||
LABEL maintainer="RustScan <https://github.com/RustScan>"
|
||||
RUN apk add --no-cache build-base
|
||||
|
||||
# Encourage some layer caching here rather then copying entire directory that includes docs to builder container ~CMN
|
||||
WORKDIR /app/rustscan
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY src/ src/
|
||||
RUN cargo build --release
|
||||
|
||||
FROM alpine:3.17
|
||||
LABEL author="Hydragyrum <https://github.com/Hydragyrum>"
|
||||
LABEL author="LeoFVO <https://github.com/LeoFVO>"
|
||||
RUN addgroup -S rustscan && \
|
||||
adduser -S -G rustscan rustscan && \
|
||||
ulimit -n 100000 && \
|
||||
apk add --no-cache nmap nmap-scripts wget
|
||||
USER rustscan
|
||||
COPY --from=builder /app/rustscan/target/release/rustscan /usr/local/bin/rustscan
|
||||
ENTRYPOINT [ "/usr/local/bin/rustscan" ]
|
||||
@@ -9,6 +9,16 @@
|
||||
|
||||
![Arch Linux package][badge-1] ![Built with Rust][badge-2] ![GitHub All Releases][badge-3] ![Crates.io][badge-4] ![Discord][badge-5] ![Actions][badge-6]
|
||||
|
||||
|
||||
---
|
||||
|
||||
| 🐋 `(Recommended)` | 👩💻 | 🏗️ | 🔧 |
|
||||
| :---------------------------------------------------------: | :---------------------------: | :-------------------------: | :-------------------------: |
|
||||
| [![Docker][DockerPic]][Docker] | [![Kali][Kali1]][Kali/Debian] | [![Arch][Arch]][Arch-Linux] | [![apple][apple]][Homebrew] |
|
||||
| `docker pull rustscan/rustscan:2.1.1`</br>[Docker][usage-0] | [Link to Documentation][kali] | `yay -S rustscan` | `brew install rustscan` |
|
||||
|
||||
---
|
||||
|
||||
</div>
|
||||
|
||||
# 🤔 What is this?
|
||||
@@ -17,32 +27,6 @@
|
||||
|
||||
The Modern Port Scanner. **Find ports quickly (3 seconds at its fastest)**. Run scripts through our scripting engine (Python, Lua, Shell supported).
|
||||
|
||||
# 🛠️ Installation
|
||||
|
||||
You can install RustScan's binary from our [releases page](https://github.com/RustScan/RustScan/releases).
|
||||
|
||||
We would prefer you to install with a package manager so it is tested and works for your system.
|
||||
|
||||
RustScan is in many repositories already. Install it with whatever tools you wish:
|
||||
|
||||
[](https://repology.org/project/rustscan/versions)
|
||||
|
||||
RustScan only officially supports Cargo installations, if you want to use that please install Rust and then `cargo install rustscan`
|
||||
|
||||
Example installations include:
|
||||
|
||||
MacOS:
|
||||
|
||||
```
|
||||
brew install rustscan
|
||||
```
|
||||
|
||||
Arch:
|
||||
|
||||
```
|
||||
pacman -S rustscan
|
||||
```
|
||||
|
||||
# ✨ Features
|
||||
|
||||
- Scans all 65k ports in **3 seconds**.
|
||||
@@ -55,7 +39,7 @@ Arch:
|
||||
|
||||
| <!--Installation Guide--> | <!--Documentation--> | <!--Discord--> |
|
||||
| :----------------------------------------: | :------------------------------------: | :-------------------------: |
|
||||
| :book: [Installation Guide][toc-install] | :books: [Documentation][links-table-2] | :parrot: [Discord][discord] |
|
||||
| :book: [Installation Guide][links-table-1] | :books: [Documentation][links-table-2] | :parrot: [Discord][discord] |
|
||||
|
||||
## 🙋 Table of Contents
|
||||
|
||||
@@ -63,6 +47,7 @@ Arch:
|
||||
- 🐋 [Docker Usage][toc-docker-usage]
|
||||
- 🦜 [Discord][discord]
|
||||
- 🤸 [Usage][usage-1]
|
||||
- 🎪 [Community][toc-community]
|
||||
|
||||
# 🔭 Why RustScan?
|
||||
|
||||
@@ -120,6 +105,21 @@ RustScan is one of the first penetration testing tools that aims to be entirely
|
||||
|
||||
RustScan has continuous integration testing that aims to ensure it is accessible, and we are constantly working on ways to improve our accessibility and ensure _everyone_ can use RustScan.
|
||||
|
||||
# 📖 Full Installation Guide
|
||||
|
||||
You can find our guide [here][install-1].
|
||||
|
||||
## 🦊 Community Distributions
|
||||
|
||||
Here are all of RustScan's community distributions.
|
||||
|
||||
If you maintain a community distribution and want it listed here, leave an issue / pull request / Discord message or however, you want to let us know.
|
||||
|
||||
- [Open Suse][distributions-1]
|
||||
- [Fedora/CentOS][distributions-2]
|
||||
|
||||
[![Packaging status][rustscan-svg]][repology-1]
|
||||
|
||||
# 🤸 Usage
|
||||
|
||||
We have 2 usage guides. [Basic Usage][usage-1] and [Things you may want to do][usage-2].
|
||||
@@ -145,7 +145,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<!-- markdownlint-disable -->
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><a href="https://skerritt.blog"><img src="https://avatars3.githubusercontent.com/u/10378052?v=4" width="100px;" alt=""/><br /><sub><b>Bee</b></sub></a><br /><a href="#infra-beeskerritt" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/RustScan/RustScan/commits?author=beeskerritt" title="Tests">⚠️</a> <a href="https://github.com/RustScan/RustScan/commits?author=beesan" title="Code">💻</a> <a href="#design-beeskerritt" title="Design">🎨</a></td>
|
||||
<td align="center"><a href="https://skerritt.blog"><img src="https://avatars3.githubusercontent.com/u/10378052?v=4" width="100px;" alt=""/><br /><sub><b>Brandon</b></sub></a><br /><a href="#infra-brandonskerritt" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/RustScan/RustScan/commits?author=brandonskerritt" title="Tests">⚠️</a> <a href="https://github.com/RustScan/RustScan/commits?author=brandonskerritt" title="Code">💻</a> <a href="#design-brandonskerritt" title="Design">🎨</a></td>
|
||||
<td align="center"><a href="https://sakiir.ovh"><img src="https://avatars1.githubusercontent.com/u/9950578?v=4" width="100px;" alt=""/><br /><sub><b>SakiiR</b></sub></a><br /><a href="https://github.com/RustScan/RustScan/commits?author=SakiiR" title="Code">💻</a> <a href="https://github.com/RustScan/RustScan/issues?q=author%3ASakiiR" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="https://github.com/smackhack"><img src="https://avatars2.githubusercontent.com/u/48143394?v=4" width="100px;" alt=""/><br /><sub><b>smackhack</b></sub></a><br /><a href="#ideas-smackhack" title="Ideas, Planning, & Feedback">🤔</a> <a href="#example-smackhack" title="Examples">💡</a></td>
|
||||
<td align="center"><a href="http://bernardoamc.github.io/"><img src="https://avatars0.githubusercontent.com/u/428984?v=4" width="100px;" alt=""/><br /><sub><b>Bernardo Araujo</b></sub></a><br /><a href="https://github.com/RustScan/RustScan/commits?author=bernardoamc" title="Code">💻</a> <a href="https://github.com/RustScan/RustScan/issues?q=author%3Abernardoamc" title="Bug reports">🐛</a> <a href="#design-bernardoamc" title="Design">🎨</a></td>
|
||||
@@ -193,7 +193,7 @@ This project follows the [all-contributors](https://github.com/all-contributors/
|
||||
[kali]: https://github.com/RustScan/RustScan/wiki/Installation-Guide#%EF%B8%8F-debian--kali "Read the install guide"
|
||||
[Kali/Debian]: https://github.com/RustScan/RustScan/releases "Kali Debian"
|
||||
[Arch-Linux]: https://archlinux.org/packages/extra/x86_64/rustscan/ "Arch Linux installation of Rustscan"
|
||||
[Homebrew]: https://formulae.brew.sh/formula/rustscan "Homebrew install of Rustscan"
|
||||
[Homebrew]: https://crates.io/crates/rustscan/ "Homebrew install of Rustscan"
|
||||
[usage-1]: https://github.com/RustScan/RustScan/wiki/Usage "Basic Usage of Rustscan"
|
||||
[usage-0]: https://github.com/RustScan/RustScan/wiki/Installation-Guide#docker-whale "Use Docker Rustscan"
|
||||
[config-file-here]: https://github.com/RustScan/RustScan/wiki/Config-File "RustScan Configuration File"
|
||||
@@ -209,7 +209,7 @@ This project follows the [all-contributors](https://github.com/all-contributors/
|
||||
[speed-3]: https://github.com/RustScan/RustScan/wiki/Increasing-Speed-&-Accuracy "Increasing Speed & Accuracy"
|
||||
[toc-community]: https://github.com/RustScan/RustScan#-community "Community"
|
||||
[links-table-1]: https://github.com/RustScan/RustScan#-full-installation-guide "Full installation guide"
|
||||
[links-table-2]: https://github.com/bee-san/RustScan/wiki "Rustscan"
|
||||
[links-table-2]: https://rustscan.github.io/RustScan/ "Rustscan"
|
||||
[discord]: http://discord.skerritt.blog "Discord blog"
|
||||
[toc-install]: https://github.com/RustScan/RustScan/wiki/Installation-Guide "Installation Guide Wiki"
|
||||
[toc-docker-usage]: https://github.com/RustScan/RustScan/wiki/Installation-Guide#docker- "Docker Installation Guide Wiki"
|
||||
@@ -232,4 +232,4 @@ This project follows the [all-contributors](https://github.com/all-contributors/
|
||||
[badge-3]: https://img.shields.io/github/downloads/rustscan/rustscan/total?label=GitHub%20Downloads
|
||||
[badge-4]: https://img.shields.io/crates/d/rustscan?label=Cargo%20Downloads
|
||||
[badge-5]: https://img.shields.io/discord/754001738184392704
|
||||
[badge-6]: https://github.com/RustScan/RustScan/actions/workflows/build.yml/badge.svg?branch=master
|
||||
[badge-6]: https://github.com/RustScan/RustScan/workflows/Continuous%20integration/badge.svg?branch=master
|
||||
@@ -1,52 +0,0 @@
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use rustscan::input::{Opts, PortRange, ScanOrder};
|
||||
use rustscan::port_strategy::PortStrategy;
|
||||
use std::net::IpAddr;
|
||||
use std::time::Duration;
|
||||
|
||||
fn bench_address() {
|
||||
let _addrs = ["127.0.0.1".parse::<IpAddr>().unwrap()];
|
||||
}
|
||||
|
||||
fn bench_port_strategy() {
|
||||
let range = PortRange {
|
||||
start: 1,
|
||||
end: 1_000,
|
||||
};
|
||||
let _strategy = PortStrategy::pick(&Some(range.clone()), None, ScanOrder::Serial);
|
||||
}
|
||||
|
||||
fn bench_address_parsing() {
|
||||
let opts = Opts {
|
||||
addresses: vec![
|
||||
"127.0.0.1".to_owned(),
|
||||
"10.2.0.1".to_owned(),
|
||||
"192.168.0.0/24".to_owned(),
|
||||
],
|
||||
exclude_addresses: Some(vec![
|
||||
"10.0.0.0/8".to_owned(),
|
||||
"172.16.0.0/12".to_owned(),
|
||||
"192.168.0.0/16".to_owned(),
|
||||
"172.16.0.1".to_owned(),
|
||||
]),
|
||||
..Default::default()
|
||||
};
|
||||
let _ips = rustscan::address::parse_addresses(&opts);
|
||||
}
|
||||
|
||||
fn criterion_benchmark(c: &mut Criterion) {
|
||||
// Benching helper functions
|
||||
c.bench_function("parse address", |b| b.iter(bench_address));
|
||||
|
||||
c.bench_function("port strategy", |b| b.iter(bench_port_strategy));
|
||||
|
||||
let mut address_group = c.benchmark_group("address parsing");
|
||||
address_group.measurement_time(Duration::from_secs(10));
|
||||
address_group.bench_function("parse addresses with exclusions", |b| {
|
||||
b.iter(bench_address_parsing)
|
||||
});
|
||||
address_group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, criterion_benchmark);
|
||||
criterion_main!(benches);
|
||||
@@ -1,234 +0,0 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs::{self, File};
|
||||
|
||||
use std::env;
|
||||
use std::io::{BufReader, Read};
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
// Reads in a file with payloads based on port
|
||||
pub fn main() {
|
||||
let mut file_path = env::current_dir().expect("cant find curr dir");
|
||||
file_path.push("./nmap-payloads");
|
||||
|
||||
let mut data = String::new();
|
||||
let file = File::open(&file_path).expect("File not found.");
|
||||
let mut file_buf = BufReader::new(file);
|
||||
file_buf
|
||||
.read_to_string(&mut data)
|
||||
.expect("unable to read file");
|
||||
|
||||
let mut fp_map: BTreeMap<i32, String> = BTreeMap::new();
|
||||
|
||||
let mut count = 0;
|
||||
let mut capturing = false;
|
||||
let mut curr = String::new();
|
||||
|
||||
for line in data.trim().split('\n') {
|
||||
if line.contains('#') || line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if line.starts_with("udp") {
|
||||
if !curr.is_empty() {
|
||||
fp_map.insert(count, curr);
|
||||
curr = String::new();
|
||||
}
|
||||
capturing = true;
|
||||
count += 1;
|
||||
}
|
||||
|
||||
if capturing {
|
||||
if !curr.is_empty() {
|
||||
curr.push(' ');
|
||||
}
|
||||
curr.push_str(line);
|
||||
}
|
||||
}
|
||||
|
||||
let pb_linenr = ports_v(&fp_map);
|
||||
let payb_linenr = payloads_v(&fp_map);
|
||||
let map = port_payload_map(pb_linenr, payb_linenr);
|
||||
|
||||
generate_code(map);
|
||||
}
|
||||
|
||||
/// Generates a file called Generated.rs and calls cargo fmt from the command line
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `port_payload_map` - A BTreeMap mapping port numbers to payload data
|
||||
fn generate_code(port_payload_map: BTreeMap<Vec<u16>, Vec<u8>>) {
|
||||
let dest_path = PathBuf::from("src/generated.rs");
|
||||
|
||||
let mut generated_code = String::new();
|
||||
generated_code.push_str("use std::collections::BTreeMap;\n");
|
||||
generated_code.push_str("use once_cell::sync::Lazy;\n\n");
|
||||
|
||||
generated_code.push_str("fn generated_data() -> BTreeMap<Vec<u16>, Vec<u8>> {\n");
|
||||
generated_code.push_str(" let mut map = BTreeMap::new();\n");
|
||||
|
||||
for (ports, payloads) in port_payload_map {
|
||||
generated_code.push_str(" map.insert(vec![");
|
||||
generated_code.push_str(
|
||||
&ports
|
||||
.iter()
|
||||
.map(|&p| p.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(","),
|
||||
);
|
||||
generated_code.push_str("], vec![");
|
||||
generated_code.push_str(
|
||||
&payloads
|
||||
.iter()
|
||||
.map(|&p| p.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(","),
|
||||
);
|
||||
generated_code.push_str("]);\n");
|
||||
}
|
||||
|
||||
generated_code.push_str(" map\n");
|
||||
generated_code.push_str("}\n\n");
|
||||
|
||||
generated_code.push_str(
|
||||
"static PARSED_DATA: Lazy<BTreeMap<Vec<u16>, Vec<u8>>> = Lazy::new(generated_data);\n",
|
||||
);
|
||||
generated_code.push_str("pub fn get_parsed_data() -> &'static BTreeMap<Vec<u16>, Vec<u8>> {\n");
|
||||
generated_code.push_str(" &PARSED_DATA\n");
|
||||
generated_code.push_str("}\n");
|
||||
|
||||
fs::write(dest_path, generated_code).unwrap();
|
||||
|
||||
// format the generated code
|
||||
Command::new("cargo")
|
||||
.arg("fmt")
|
||||
.arg("--all")
|
||||
.output()
|
||||
.expect("Failed to execute cargo fmt");
|
||||
}
|
||||
|
||||
/// Creates a BTreeMap of line numbers mapped to a Vec<u16> of ports
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `fp_map` - A BTreeMap containing the parsed file data
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A BTreeMap where keys are line numbers and values are vectors of ports
|
||||
fn ports_v(fp_map: &BTreeMap<i32, String>) -> BTreeMap<i32, Vec<u16>> {
|
||||
let mut pb_linenr: BTreeMap<i32, Vec<u16>> = BTreeMap::new();
|
||||
let mut port_list: Vec<u16> = Vec::new();
|
||||
|
||||
for (&line_nr, ports) in fp_map {
|
||||
if ports.contains("udp ") {
|
||||
let remain = &ports[4..];
|
||||
let mut start = remain.split(' ');
|
||||
|
||||
let ports = start.next().unwrap();
|
||||
let port_segments: Vec<&str> = ports.split(',').collect();
|
||||
|
||||
for segment in port_segments {
|
||||
if segment.contains('-') {
|
||||
let range: Vec<&str> = segment.trim().split('-').collect();
|
||||
let start = range[0].parse::<u16>().unwrap();
|
||||
let end = range[1].parse::<u16>().unwrap();
|
||||
|
||||
for port in start..end {
|
||||
port_list.push(port);
|
||||
}
|
||||
} else if !segment.is_empty() {
|
||||
match segment.parse::<u16>() {
|
||||
Ok(port) => port_list.push(port),
|
||||
Err(_) => println!("Error parsing port: {segment}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pb_linenr.insert(line_nr, port_list.clone());
|
||||
port_list.clear();
|
||||
}
|
||||
|
||||
pb_linenr
|
||||
}
|
||||
|
||||
/// Parses out the Payloads into a BTreeMap of line numbers mapped to vectors of payload bytes
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `fp_map` - A BTreeMap containing the parsed file data
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A BTreeMap where keys are line numbers and values are vectors of payload bytes
|
||||
fn payloads_v(fp_map: &BTreeMap<i32, String>) -> BTreeMap<i32, Vec<u8>> {
|
||||
let mut payb_linenr: BTreeMap<i32, Vec<u8>> = BTreeMap::new();
|
||||
|
||||
for (&line_nr, data) in fp_map {
|
||||
if data.contains('\"') {
|
||||
let start = data.find('\"').expect("payload opening \" not found");
|
||||
let payloads = &data[start + 1..];
|
||||
payb_linenr.insert(line_nr, parser(payloads.trim()));
|
||||
}
|
||||
}
|
||||
|
||||
payb_linenr
|
||||
}
|
||||
|
||||
/// Converts a hexadecimal string to a Vec<u8>
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `payload` - A string slice containing the hexadecimal payload
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A vector of bytes representing the decoded payload
|
||||
fn parser(payload: &str) -> Vec<u8> {
|
||||
let payload = payload.trim_matches('"');
|
||||
let mut tmp_str = String::new();
|
||||
let mut bytes: Vec<u8> = Vec::new();
|
||||
|
||||
for (idx, char) in payload.chars().enumerate() {
|
||||
if char == '\\' && payload.chars().nth(idx + 1) == Some('x') {
|
||||
continue;
|
||||
} else if char.is_ascii_hexdigit() {
|
||||
tmp_str.push(char);
|
||||
if tmp_str.len() == 2 {
|
||||
bytes.push(u8::from_str_radix(&tmp_str, 16).unwrap());
|
||||
tmp_str.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bytes
|
||||
}
|
||||
|
||||
/// Combines the ports BTreeMap and the Payloads BTreeMap
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `pb_linenr` - A BTreeMap mapping line numbers to vectors of ports
|
||||
/// * `payb_linenr` - A BTreeMap mapping line numbers to vectors of payload bytes
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A BTreeMap mapping vectors of ports to vectors of payload bytes
|
||||
fn port_payload_map(
|
||||
pb_linenr: BTreeMap<i32, Vec<u16>>,
|
||||
payb_linenr: BTreeMap<i32, Vec<u8>>,
|
||||
) -> BTreeMap<Vec<u16>, Vec<u8>> {
|
||||
let mut ppm_fin: BTreeMap<Vec<u16>, Vec<u8>> = BTreeMap::new();
|
||||
|
||||
for (port_linenr, ports) in pb_linenr {
|
||||
for (pay_linenr, payloads) in &payb_linenr {
|
||||
if pay_linenr == &port_linenr {
|
||||
ppm_fin.insert(ports.to_vec(), payloads.to_vec());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ppm_fin
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
# Dockerfile for RustScan development environment
|
||||
# Provides a containerized setup with Rust, nmap, and development tools
|
||||
FROM rust
|
||||
# Install nmap first.
|
||||
RUN apt-get update -qy && apt-get install -qy nmap
|
||||
# Then install rustfmt for code formatting and clippy for linting.
|
||||
# Then install rustfmt and clippy for cargo.
|
||||
RUN rustup component add rustfmt clippy
|
||||
@@ -67,8 +67,3 @@ root@container:/rustscan# cargo fmt
|
||||
root@container:/rustscan# cargo clippy
|
||||
root@container:/rustscan# cargo test
|
||||
```
|
||||
|
||||
Automated tests and benchmarks must run without network traffic or external script
|
||||
execution. Keep coverage focused on parsing, configuration, port ordering, socket
|
||||
address iteration, and timing helpers. Do not add tests that run TCP/UDP scans
|
||||
(including localhost), resolve hostnames, or launch Nmap or other scripts.
|
||||
@@ -1,4 +0,0 @@
|
||||
tags = ["core_approved", "example"]
|
||||
ports = ["80", "443", "8080"]
|
||||
developer = ["example", "https://example.org"]
|
||||
directory = "fixtures/.rustscan_scripts"
|
||||
@@ -4,7 +4,7 @@
|
||||
#ports_separator = ","
|
||||
#call_format = "perl {{script}} {{ip}} {{port}}"
|
||||
|
||||
# The Scriptfile parser stops parsing at the first blank line.
|
||||
# Sriptfile parser stops at the first blank line with parsing.
|
||||
# This script will run itself as an argument with the system installed perl interpreter, ports will be concatenated with "," .
|
||||
# Unused field: trigger_port = "80"
|
||||
# get total arg passed to this script
|
||||
@@ -20,4 +20,4 @@ print "Total args passed to $scriptname : $total\n";
|
||||
foreach my $a(@ARGV) {
|
||||
print "Arg # $counter : $a\n";
|
||||
$counter++;
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,10 @@
|
||||
#trigger_port = "80"
|
||||
#call_format = "python3 {{script}} {{ip}} {{port}}"
|
||||
|
||||
# The Scriptfile parser stops parsing at the first blank line.
|
||||
# Sriptfile parser stops at the first blank line with parsing.
|
||||
# This script will run itself as an argument with the system installed python interpreter, only scanning port 80.
|
||||
# Unused filed: ports_separator = ","
|
||||
|
||||
import sys
|
||||
|
||||
print('Python script ran with arguments', str(sys.argv))
|
||||
print('Python script ran with arguments', str(sys.argv))
|
||||
@@ -4,9 +4,9 @@
|
||||
#ports_separator = ","
|
||||
#call_format = "bash {{script}} {{ip}} {{port}}"
|
||||
|
||||
# The Scriptfile parser stops parsing at the first blank line.
|
||||
# Sriptfile parser stops at the first blank line with parsing.
|
||||
# This script will run itself as an argument with the system installed bash interpreter, scanning all ports concatenated with "," .
|
||||
# Unused filed: trigger_port = "80"
|
||||
|
||||
# print all arguments passed to the script
|
||||
echo $@
|
||||
echo $@
|
||||
@@ -4,6 +4,6 @@
|
||||
#ports_separator = ","
|
||||
#call_format = "nmap -vvv -p {{port}} {{ip}}"
|
||||
|
||||
# The Scriptfile parser stops parsing at the first blank line.
|
||||
# Scriptfile parser stops at the first blank line with parsing.
|
||||
# This script will run the system installed nmap, ports will be concatenated with "," .
|
||||
# Unused field: trigger_port = "80"
|
||||
# Unused field: trigger_port = "80"
|
||||
@@ -1,18 +0,0 @@
|
||||
test:
|
||||
cargo test
|
||||
cargo clippy -- --deny warnings
|
||||
cargo clippy --tests -- --deny warnings
|
||||
cargo fmt --check
|
||||
cargo doc --workspace --all-features --no-deps --document-private-items
|
||||
|
||||
fix:
|
||||
cargo fmt
|
||||
cargo clippy --fix
|
||||
|
||||
# release only runs if test runs
|
||||
release: test
|
||||
git checkout master
|
||||
git pull
|
||||
cargo generate-lockfile
|
||||
cargo publish
|
||||
git push
|
||||
-500
@@ -1,500 +0,0 @@
|
||||
# Nmap nmap payload database -*- mode: fundamental; -*-
|
||||
# $Id$
|
||||
#
|
||||
# These payloads are sent with every host discovery or port scan probe
|
||||
# by default. This database should only include payloads that are
|
||||
# unlikely to crash services, trip IDS alerts, or change state on the
|
||||
# server. The idea behind these is to evoke a response using a payload.
|
||||
# Some of them are taken from nmap-service-probes.
|
||||
#
|
||||
# This collection of data is (C) 1996-2022 by Nmap Software LLC. It
|
||||
# is distributed under the Nmap Public Source license as provided in
|
||||
# the LICENSE file of the source distribution or at
|
||||
# https://nmap.org/npsl/. Note that this free license does not allow
|
||||
# incorporation of Nmap or its data files within proprietary
|
||||
# software. We sell a separate Nmap OEM for that as described
|
||||
# (including pricing) at https://nmap.org/npsl/.
|
||||
#
|
||||
# Each entry begins with a protocol (only "udp" is supported) followed
|
||||
# by a comma-separated list of ports, followed by one or more quoted
|
||||
# strings containing the payload. These elements may be broken across
|
||||
# several lines. For future expansion, additional keywords may follow
|
||||
# the payload data. Any data following one of these keywords must be on
|
||||
# the same line as the keyword so that unknown keywords can be ignored
|
||||
# by the parser. Currently this file contains some entries with the
|
||||
# "source" keyword to specify a desired source port, but it is not
|
||||
# honored by Nmap.
|
||||
#
|
||||
# Multiple payloads may be defined for a single protocol and port, in
|
||||
# which case they will be all be sent concurrently. There is a limit
|
||||
# of 255 payloads per port.
|
||||
#
|
||||
# Lines longer than 1024 characters will be ignored.
|
||||
#
|
||||
# Example:
|
||||
# udp 1234 "payloaddatapayloaddata"
|
||||
# "payloaddatapayloaddata"
|
||||
# source 5678
|
||||
|
||||
# GenericLines. Use for the echo service.
|
||||
udp 7 "\x0D\x0A\x0D\x0A"
|
||||
# DNSStatusRequest
|
||||
# Sent for TFTP (69) as well due to its ability to provoke an error response
|
||||
udp 53,69,5353,26198 "\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
# DNS VER
|
||||
udp 53,5353,26198
|
||||
"\x77\x77\x01\x00\x00"
|
||||
"\x01\x00\x00\x00\x00\x00\x00\x07version\x04bind\x00\x00\x10\x00\x03"
|
||||
|
||||
# DHCP INFORM
|
||||
udp 67
|
||||
"\x01\x01\x06\x00"
|
||||
"\x01\x23\x45\x67\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x35\xd4\xd8\x51\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x63\x82\x53\x63\x35\x01"
|
||||
"\x08\xff"
|
||||
|
||||
# TFTP GET
|
||||
udp 69 "\x00\x01r7tftp.txt\x00octet\x00"
|
||||
|
||||
# QUIC packet with unsupported version Q999
|
||||
# Also found on 443, but need to check whether DTLS or QUIC is more prevalent
|
||||
udp 80 "\r12345678Q999\x00"
|
||||
# RPCCheck
|
||||
udp 111
|
||||
"\x72\xFE\x1D\x13\x00\x00\x00\x00\x00\x00\x00\x02\x00\x01\x86\xA0"
|
||||
"\x00\x01\x97\x7C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
# ONCRPC CALL
|
||||
udp 111,2049,4045,32768-65535
|
||||
"\x3e\xec\xe3\xca\x00\x00\x00\x00\x00\x00\x00\x02\x00"
|
||||
"\xbc\x61\x4e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
# NTPRequest
|
||||
udp 123
|
||||
"\xE3\x00\x04\xFA\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\xC5\x4F\x23\x4B\x71\xB1\x52\xF3"
|
||||
# NTP REQ
|
||||
udp 123
|
||||
"\xd9\x00\x0a\xfa\x00\x00\x00"
|
||||
"\x00\x00\x01\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc6"
|
||||
"\xf1\x5e\xdb\x78\x00\x00\x00"
|
||||
# DCERPC CALL
|
||||
udp 135,1025-1199
|
||||
"\x05\x00\x0b\x03\x10\x00\x00\x00\x48\x00"
|
||||
"\x00\x00\x01\x00\x00\x00\xb8\x10\xb8\x10\x00\x00\x00\x00\x01\x00\x00"
|
||||
"\x00\x00\x00\x01\x00\x01\x23\x45\x67\x89\xab\xcd\xef\x01\x23\x45\x67"
|
||||
"\x89\xab\xcd\xef\xe7\x03\x00\x00\xfe\xdc\xba\x98\x76\x54\x32\x10\x01"
|
||||
"\x23\x45\x67\x89\xab\xcd\xef\xe7\x03\x00\x00"
|
||||
# NBTStat
|
||||
udp 137
|
||||
"\x80\xF0\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00"
|
||||
"\x20CKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00\x00\x21\x00\x01"
|
||||
# CIFS NS NAME QUERY UC
|
||||
udp 137
|
||||
"\x01\x91\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00"
|
||||
"\x20CKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00\x00\x21\x00\x01"
|
||||
# CIFS NS NAME QUERY BC
|
||||
udp 137
|
||||
"\x01\x91\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00"
|
||||
"\x20CKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00\x00\x21\x00\x01"
|
||||
# SNMPv3GetRequest
|
||||
udp 161,260,3401
|
||||
"\x30\x3A\x02\x01\x03\x30\x0F\x02\x02\x4A\x69\x02\x03\x00\xFF\xE3"
|
||||
"\x04\x01\x04\x02\x01\x03\x04\x10\x30\x0E\x04\x00\x02\x01\x00\x02"
|
||||
"\x01\x00\x04\x00\x04\x00\x04\x00\x30\x12\x04\x00\x04\x00\xA0\x0C"
|
||||
"\x02\x02\x37\xF0\x02\x01\x00\x02\x01\x00\x30\x00"
|
||||
# SNMP PUBLIC WALK
|
||||
udp 161,260,3401
|
||||
"\x30\x1f\x02\x01\x00\x04\x06public\xa1\x12\x02\x01\x00\x02"
|
||||
"\x01\x00\x02\x01\x00\x30\x07\x30\x05\x06\x01\x00\x05\x00"
|
||||
|
||||
# Sqlping - disabled because it trips a Snort rule with SID 2049
|
||||
# ("MS-SQL ping attempt").
|
||||
# udp 1434 "\x02"
|
||||
|
||||
# xdmcp - X Display Manager Control Protocol. Version 1, packet type
|
||||
# Query (2), no authorization names. We expect a Willing or Unwilling
|
||||
# packet in reply.
|
||||
# http://cgit.freedesktop.org/xorg/doc/xorg-docs/plain/hardcopy/XDMCP/xdmcp.PS.gz
|
||||
udp 177 "\x00\x01\x00\x02\x00\x01\x00"
|
||||
|
||||
# Connectionless LDAP - used by Microsoft Active Directory
|
||||
udp 389
|
||||
"\x30\x84\x00\x00\x00\x2d\x02\x01\x07\x63\x84\x00\x00\x00\x24\x04\x00"
|
||||
"\x0a\x01\x00\x0a\x01\x00\x02\x01\x00\x02\x01\x64\x01\x01\x00\x87\x0b"
|
||||
"objectClass0\x84\x00\x00\x00\x00"
|
||||
|
||||
|
||||
# svrloc
|
||||
udp 427
|
||||
"\x02\x01\x00\x006 \x00\x00\x00\x00\x00\x01\x00\x02en\x00\x00\x00\x15"
|
||||
"service:service-agent\x00\x07default\x00\x00\x00\x00"
|
||||
|
||||
# DTLS
|
||||
udp 443,853,3391,4433,4740,5349,5684,5868,6514,6636,8232,10161,10162,12346,12446,12546,12646,12746,12846,12946,13046
|
||||
# DTLS 1.0, length 52
|
||||
"\x16\xfe\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x36"
|
||||
# ClientHello, length 40, sequence 0, offset 0
|
||||
"\x01\x00\x00\x2a\x00\x00\x00\x00\x00\x00\x00\x2a"
|
||||
# DTLS 1.2
|
||||
"\xfe\xfd"
|
||||
# Random
|
||||
"\x00\x00\x00\x00\x7c\x77\x40\x1e\x8a\xc8\x22\xa0\xa0\x18\xff\x93"
|
||||
"\x08\xca\xac\x0a\x64\x2f\xc9\x22\x64\xbc\x08\xa8\x16\x89\x19\x3f"
|
||||
# Session id length 0, cookie length 0
|
||||
"\x00\x00"
|
||||
# Cipher suites, mandatory TLS_RSA_WITH_AES_128_CBC_SHA
|
||||
"\x00\x02\x00\x2f"
|
||||
# Compressors (NULL)
|
||||
"\x01\x00"
|
||||
|
||||
# Internet Key Exchange version 1, phase 1 Main Mode. We offer every
|
||||
# combination of (DES, 3DES) and (MD5, SHA) in the hope that one of them will
|
||||
# be acceptable. Because we use a fixed cookie, we set the association lifetime
|
||||
# to 1 second to reduce the chance that repeated probes will look like
|
||||
# retransmissions (and therefore not get a response). This payload comes from
|
||||
# ike-scan --lifetime 1 --cookie 0011223344556677 --trans=5,2,1,2 --trans=5,1,1,2 --trans=1,2,1,2 --trans=1,1,1,2
|
||||
# We expect another phase 1 message in response. This payload works better with
|
||||
# a source port of 500 or a randomized initiator cookie.
|
||||
udp 500
|
||||
# Initiator cookie 0x0011223344556677, responder cookie 0x0000000000000000.
|
||||
"\x00\x11\x22\x33\x44\x55\x66\x77\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
# Version 1, Main Mode, flags 0x00, message ID 0x00000000, length 192.
|
||||
"\x01\x10\x02\x00\x00\x00\x00\x00\x00\x00\x00\xC0"
|
||||
# Security Association payload, length 164, IPSEC, IDENTITY.
|
||||
"\x00\x00\x00\xA4\x00\x00\x00\x01\x00\x00\x00\x01"
|
||||
# Proposal 1, length 152, ISAKMP, 4 transforms.
|
||||
"\x00\x00\x00\x98\x01\x01\x00\x04"
|
||||
# Transform 1, 3DES-CBC, SHA, PSK, group 2.
|
||||
"\x03\x00\x00\x24\x01\x01\x00\x00\x80\x01\x00\x05\x80\x02\x00\x02"
|
||||
"\x80\x03\x00\x01\x80\x04\x00\x02"
|
||||
"\x80\x0B\x00\x01\x00\x0C\x00\x04\x00\x00\x00\x01"
|
||||
# Transform 2, 3DES-CBC, MD5, PSK, group 2.
|
||||
"\x03\x00\x00\x24\x02\x01\x00\x00\x80\x01\x00\x05\x80\x02\x00\x01"
|
||||
"\x80\x03\x00\x01\x80\x04\x00\x02"
|
||||
"\x80\x0B\x00\x01\x00\x0C\x00\x04\x00\x00\x00\x01"
|
||||
# Transform 3, DES-CBC, SHA, PSK, group 2.
|
||||
"\x03\x00\x00\x24\x03\x01\x00\x00\x80\x01\x00\x01\x80\x02\x00\x02"
|
||||
"\x80\x03\x00\x01\x80\x04\x00\x02"
|
||||
"\x80\x0B\x00\x01\x00\x0C\x00\x04\x00\x00\x00\x01"
|
||||
# Transform 4, DES-CBC, MD5, PSK, group 2.
|
||||
"\x00\x00\x00\x24\x04\x01\x00\x00\x80\x01\x00\x01\x80\x02\x00\x01"
|
||||
"\x80\x03\x00\x01\x80\x04\x00\x02"
|
||||
"\x80\x0B\x00\x01\x00\x0C\x00\x04\x00\x00\x00\x01"
|
||||
# IPSEC START
|
||||
udp 500,4500
|
||||
"\x31\x27\xfc"
|
||||
"\xb0\x38\x10\x9e\x89\x00\x00\x00\x00\x00\x00\x00\x00\x01\x10\x02\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\xcc\x0d\x00\x00\x5c\x00\x00\x00\x01\x00"
|
||||
"\x00\x00\x01\x00\x00\x00\x50\x01\x01\x00\x02\x03\x00\x00\x24\x01\x01"
|
||||
"\x00\x00\x80\x01\x00\x05\x80\x02\x00\x02\x80\x04\x00\x02\x80\x03\x00"
|
||||
"\x03\x80\x0b\x00\x01\x00\x0c\x00\x04\x00\x00\x0e\x10\x00\x00\x00\x24"
|
||||
"\x02\x01\x00\x00\x80\x01\x00\x05\x80\x02\x00\x01\x80\x04\x00\x02\x80"
|
||||
"\x03\x00\x03\x80\x0b\x00\x01\x00\x0c\x00\x04\x00\x00\x0e\x10\x0d\x00"
|
||||
"\x00\x18\x1e\x2b\x51\x69\x05\x99\x1c\x7d\x7c\x96\xfc\xbf\xb5\x87\xe4"
|
||||
"\x61\x00\x00\x00\x04\x0d\x00\x00\x14\x40\x48\xb7\xd5\x6e\xbc\xe8\x85"
|
||||
"\x25\xe7\xde\x7f\x00\xd6\xc2\xd3\x0d\x00\x00\x14\x90\xcb\x80\x91\x3e"
|
||||
"\xbb\x69\x6e\x08\x63\x81\xb5\xec\x42\x7b\x1f\x00\x00\x00\x14\x26\x24"
|
||||
"\x4d\x38\xed\xdb\x61\xb3\x17\x2a\x36\xe3\xd0\xcf\xb8\x19"
|
||||
|
||||
# Routing Information Protocol version 1. Special-case request for the entire
|
||||
# routing table (address family 0, address 0.0.0.0, metric 16). RFC 1058,
|
||||
# section 3.4.1.
|
||||
udp 520
|
||||
"\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x10"
|
||||
|
||||
# RMCP ASF ping
|
||||
udp 623
|
||||
"\x06\x00\xff\x06" # RMCP version 6, sequence 0xff, normal RMCP class ASF
|
||||
"\x00\x00\x11\xbe" # IAN ASF code 4542
|
||||
"\x80\x00\x00\x00" # payload-less ASF presence ping
|
||||
|
||||
# IPMI
|
||||
# RMCP Get Channel Auth Capabilities
|
||||
udp 623
|
||||
"\x06\x00\xff\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x09\x20\x18"
|
||||
"\xc8\x81\x00\x38\x8e\x04\xb5"
|
||||
|
||||
# serialnumberd. This service runs on Mac OS X Server. This probe
|
||||
# requests the serial number of another server. In response we expect a
|
||||
# packet starting with "SNRESPS:", followed by some data whose purpose
|
||||
# is not known.
|
||||
udp 626 "SNQUERY: 127.0.0.1:AAAAAA:xsvr"
|
||||
|
||||
# OpenVPN P_CONTROL_HARD_RESET_CLIENT_V2
|
||||
# Byte 0; 0x38 opcode
|
||||
# Byte 1-8: Session ID, random
|
||||
# Byte 9: Message packet-id array length (0)
|
||||
# Byte 10-13: Message packet-id (0)
|
||||
udp 1194 "8d\xc1x\x01\xb8\x9b\xcb\x8f\0\0\0\0\0"
|
||||
# OpenVPN when in PKI mode and without the "HMAC Firewall" setting enabled
|
||||
# (tls-auth) should respond to the following probe, which is
|
||||
# 0x38<8 random bytes><4 null bytes>
|
||||
udp 1194
|
||||
"\x38\x01\x02\x03\x04\x05\x06\x07\x08\x00\x00\x00\x00"
|
||||
|
||||
# Citrix MetaFrame application browser service
|
||||
# Original idea from http://sh0dan.org/oldfiles/hackingcitrix.html
|
||||
# Payload contents copied from Wireshark capture of Citrix Program
|
||||
# Neighborhood client application. The application uses this payload to
|
||||
# locate Citrix servers on the local network. Response to this probe is
|
||||
# a 48 byte UDP payload as shown here:
|
||||
#
|
||||
# 0000 30 00 02 31 02 fd a8 e3 02 00 06 44 c0 a8 80 55
|
||||
# 0010 00 00 00 00 00 00 00 00 00 00 00 00 02 00 06 44
|
||||
# 0020 c0 a8 80 56 00 00 00 00 00 00 00 00 00 00 00 00
|
||||
#
|
||||
# The first 12 bytes appear to be the same in all responses.
|
||||
#
|
||||
# Bytes 0x00 appears to be a packet length field
|
||||
# Bytes 0x0C - 0x0F are the IP address of the server
|
||||
# Bytes 0x10 - 0x13 may vary, 0x14 - 0x1F do not appear to
|
||||
# Bytes 0x20 - 0x23 are the IP address of the primary system in a server farm
|
||||
# configuration
|
||||
# Bytes 0x24 - 0x27 can vary, 0x28 - 0x2F do not appear to
|
||||
udp 1604
|
||||
"\x1e\x00\x01\x30\x02\xfd\xa8\xe3\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
|
||||
# RADIUS Access-Request. This is a degenerate packet with no username or
|
||||
# password; we expect an Access-Reject in response. The Identifier and Request
|
||||
# Authenticator are both 0. It was generated by running
|
||||
# echo 'User-Password = ""' | radclient <ip> auth ""
|
||||
# and then manually stripping out the password.
|
||||
#
|
||||
# Section 2 of the RFC says "A request from a client for which the
|
||||
# RADIUS server does not have a shared secret MUST be silently
|
||||
# discarded." So this payload only works when the server is configured
|
||||
# (or misconfigured) to know the scanning machine as a client.
|
||||
#
|
||||
# RFC 2865: "The early deployment of RADIUS was done using UDP port
|
||||
# number 1645, which conflicts with the "datametrics" service. The
|
||||
# officially assigned port number for RADIUS is 1812.
|
||||
udp 1645,1812
|
||||
"\x01\x00\x00\x14"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
|
||||
# L2TP ICRQ
|
||||
udp 1701
|
||||
"\xc8\x02"
|
||||
"\x00\x3c\x00\x00\x00\x00\x00\x00\x00\x00\x80\x08\x00\x00\x00\x00\x00"
|
||||
"\x01\x80\x08\x00\x00\x00\x02\x01\x00\x80\x0e\x00\x00\x00\x07"
|
||||
"nxp-scan\x80\x0a\x00\x00\x00\x03\x00\x00\x00\x03\x80"
|
||||
"\x08\x00\x00\x00\x09\x00\x00"
|
||||
|
||||
# UPNP MSEARCH
|
||||
udp 1900
|
||||
"M-SEARCH * HTTP/1.1\r\nHost: 239.255.255.250:1900\r\n"
|
||||
"Man: \"ssdp:discover\"\r\nMX: 5\r\nST: ssdp:all\r\n\r\n"
|
||||
|
||||
# NFS version 2, RFC 1831. XID 0x00000000, program 100003 (NFS), procedure
|
||||
# NFSPROC_NULL (does nothing, see section 2.2.1), null authentication (see
|
||||
# section 9.1).
|
||||
udp 2049
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x01\x86\xA3"
|
||||
"\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
|
||||
# GPRS Tunneling Protocol (GTP)
|
||||
udp 2123,2152
|
||||
# GTPv1, protocol 1
|
||||
"\x32"
|
||||
# EchoRequest
|
||||
"\x01"
|
||||
# message length
|
||||
"\x00\x04"
|
||||
# Tunnel endpoint identifier
|
||||
"\x00\x00\x42\x00"
|
||||
# sequence number
|
||||
"\x13\x37"
|
||||
# N-PDU number
|
||||
"\x00"
|
||||
# next extension header type
|
||||
"\x00"
|
||||
|
||||
# GPRS Tunneling Protocol (GTP) "prime" v2
|
||||
# This same packet can be used for GTP v2 on ports 2123 and 2152 if you change
|
||||
# the first byte from \x4e to \x40
|
||||
udp 3386
|
||||
# GTP'v2
|
||||
"\x4e"
|
||||
# EchoRequest
|
||||
"\x01"
|
||||
# message length
|
||||
"\x00\x04"
|
||||
# sequence number
|
||||
"\xde\xfe\xc8\x00"
|
||||
|
||||
# Freelancer game server status query
|
||||
# http://sourceforge.net/projects/gameq/
|
||||
# (relevant files: games.ini, packets.ini, freelancer.php)
|
||||
udp 2302 "\x00\x02\xf1\x26\x01\x26\xf0\x90\xa6\xf0\x26\x57\x4e\xac\xa0\xec\xf8\x68\xe4\x8d\x21"
|
||||
|
||||
# Apple Remote Desktop (ARD)
|
||||
udp 3283 "\0\x14\0\x01\x03"
|
||||
|
||||
# STUN Binding request, see RFC 5389 Section 6
|
||||
# message type = 0x001, Binding (see Section 18.1)
|
||||
# message length = 0
|
||||
# magic cookie = 0x2112a442
|
||||
# transaction ID = "\x00"*12
|
||||
udp 3478 "\x00\x01\x00\x00\x21\x12\xa4\x42\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
|
||||
# Sun Service Tag Discovery protocol (stdiscover)
|
||||
# http://arc.opensolaris.org/caselog/PSARC/2006/638/stdiscover_protocolv2.pdf
|
||||
# Would work better with a varying cookie; the second and later sends of this
|
||||
# probe will be interpreted as resends by the server and will be ignored.
|
||||
udp 6481 "[PROBE] 0000"
|
||||
|
||||
# NAT-PMP external IP address request. See section 3.2 of
|
||||
# http://files.dns-sd.org/draft-cheshire-nat-pmp.txt.
|
||||
udp 5351 "\x00\x00"
|
||||
|
||||
# DNS Service Discovery (DNS-SD) service query, as used in Zeroconf.
|
||||
# Transaction ID 0x0000, flags 0x0000, 1 question: PTR query for
|
||||
# _services._dns-sd._udp.local. If the remote host supports DNS-SD it will send
|
||||
# back a list of all its services. This is the same as a packet capture of
|
||||
# dns-sd -B _services._dns-sd._udp .
|
||||
# See section 9 of
|
||||
# http://files.dns-sd.org/draft-cheshire-dnsext-dns-sd.txt.
|
||||
# This first probe is a QU probe, meaning a unicast response is desired
|
||||
udp 5353
|
||||
"\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00"
|
||||
"\x09_services\x07_dns-sd\x04_udp\x05local\x00\x00\x0C\x80\x01"
|
||||
# This second probe is a QM probe, meaning a unicast response is desired
|
||||
udp 5353
|
||||
"\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00"
|
||||
"\x09_services\x07_dns-sd\x04_udp\x05local\x00\x00\x0C\x00\x01"
|
||||
|
||||
# PCANY STATUS
|
||||
udp 5632 "ST"
|
||||
|
||||
# CoAP GET .well-known/core
|
||||
udp 5683 "@\x01\x01\xce\xbb.well-known\x04core"
|
||||
|
||||
# UT2K PING
|
||||
udp 7777 "None\x00"
|
||||
|
||||
# Ubiquiti Discovery Service - v1
|
||||
udp 10001 "\x01\x00\x00\x00"
|
||||
|
||||
# Amanda backup service noop request. I think that this does nothing on the
|
||||
# server but only asks it to send back its feature list. In reply we expect an
|
||||
# ACK or (more likely) an ERROR. I couldn't find good online documentation of
|
||||
# the Amanda network protocol. There is parsing code in the Amanda source at
|
||||
# common-src/security-util.c. This is based on a packet capture of
|
||||
# amcheck <config> <host>
|
||||
udp 10080
|
||||
"Amanda 2.6 REQ HANDLE 000-00000000 SEQ 0\n"
|
||||
"SERVICE noop\n"
|
||||
|
||||
# VxWorks Wind River Debugger
|
||||
udp 17185
|
||||
# Random XID
|
||||
"\x00\x00\x00\x00"
|
||||
# RPC version 2 procedure call
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x02"
|
||||
# WDB version 1
|
||||
"\x55\x55\x55\x55\x00\x00\x00\x01"
|
||||
# WDB_TARGET_PING
|
||||
"\x00\x00\x00\x00"
|
||||
# RPC Auth NULL
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
# Checksum
|
||||
"\xff\xff\x55\x13"
|
||||
# WDB wrapper (length and sequence number)
|
||||
"\x00\x00\x00\x30\x00\x00\x00\x01"
|
||||
# Empty data?
|
||||
"\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
# VXWORKS DEBUG (alternative?)
|
||||
udp 17185
|
||||
"\x72\x37\x72\x37\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x02\x55\x55\x55\x55\x00\x00\x00\x01\x00\x00\x00\x01"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff"
|
||||
"\xff\x55\x10\x00\x00\x00\x3c\x00\x00\x00\x03\x00\x00\x00\x02\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00"
|
||||
|
||||
# Quake 2 and Quake 3 game servers (and servers of derived games like Nexuiz).
|
||||
# Gets game information from the server (see probe responses in
|
||||
# nmap-service-probes). These services typically run on a base port or a
|
||||
# few numbers higher.
|
||||
# Quake 2. Typical ports: 27910-27914.
|
||||
udp 27910-27914 "\xff\xff\xff\xffstatus"
|
||||
# Quake 3. Typical ports:
|
||||
# 26000-26004: Nexuiz
|
||||
# 27960-27964: Various games
|
||||
# 30720-30724: Tremulous
|
||||
# 44400: Warsow
|
||||
udp 26000-26004,27960-27964,30720-30724,44400 "\xff\xff\xff\xffgetstatus"
|
||||
|
||||
# Murmur 1.2.X (Mumble server)
|
||||
# UDP ping. "abcdefgh" is an identifier. See
|
||||
# http://mumble.sourceforge.net/Protocol.
|
||||
udp 64738 "\x00\x00\x00\x00abcdefgh"
|
||||
|
||||
# Ventrilo 2.1.2+
|
||||
# UDP general status request (encrypted).
|
||||
# See http://aluigi.altervista.org/papers.htm#ventrilo
|
||||
udp 3784
|
||||
"\x01\xe7\xe5\x75\x31\xa3\x17\x0b\x21\xcf\xbf\x2b\x99\x4e\xdd\x19\xac\xde\x08\x5f\x8b\x24\x0a\x11\x19\xb6\x73\x6f\xad\x28\x13\xd2\x0a\xb9\x12\x75"
|
||||
|
||||
# Kademlia (kad) as used by various P2P applications. Send a Kademlia ping
|
||||
# 4665, 4666, 4672, 6429: eDonkey/eMule and variants
|
||||
udp 4665,4666,4672,6429 "\xE4\x60"
|
||||
|
||||
# TeamSpeak 2
|
||||
# UDP login request
|
||||
# See http://wiki.wireshark.org/TeamSpeak2
|
||||
udp 8767
|
||||
"\xf4\xbe\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x002x\xba\x85\tTeamSpeak\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\nWindows XP\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00 \x00<\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08nickname\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
|
||||
# TS3INIT1
|
||||
udp 9987
|
||||
"TS3INIT1\x00\x65\x00\x00\x88\x0a\x39\x7b\x0f\x00\x5b\x55\x72\xef\xdc\x78\x32\x6b\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
|
||||
# TeamSpeak 3
|
||||
# UDP login request (encrypted)
|
||||
# http://seclists.org/nmap-dev/2013/q3/72
|
||||
udp 9987
|
||||
"\x05\xca\x7f\x16\x9c\x11\xf9\x89\x00\x00\x00\x00\x02\x9d\x74\x8b\x45\xaa\x7b\xef\xb9\x9e\xfe\xad\x08\x19\xba\xcf\x41\xe0\x16\xa2\x32\x6c\xf3\xcf\xf4\x8e\x3c\x44\x83\xc8\x8d\x51\x45\x6f\x90\x95\x23\x3e\x00\x97\x2b\x1c\x71\xb2\x4e\xc0\x61\xf1\xd7\x6f\xc5\x7e\xf6\x48\x52\xbf\x82\x6a\xa2\x3b\x65\xaa\x18\x7a\x17\x38\xc3\x81\x27\xc3\x47\xfc\xa7\x35\xba\xfc\x0f\x9d\x9d\x72\x24\x9d\xfc\x02\x17\x6d\x6b\xb1\x2d\x72\xc6\xe3\x17\x1c\x95\xd9\x69\x99\x57\xce\xdd\xdf\x05\xdc\x03\x94\x56\x04\x3a\x14\xe5\xad\x9a\x2b\x14\x30\x3a\x23\xa3\x25\xad\xe8\xe6\x39\x8a\x85\x2a\xc6\xdf\xe5\x5d\x2d\xa0\x2f\x5d\x9c\xd7\x2b\x24\xfb\xb0\x9c\xc2\xba\x89\xb4\x1b\x17\xa2\xb6"
|
||||
|
||||
# Memcached
|
||||
# version request (shorter response than stats)
|
||||
# https://github.com/memcached/memcached/blob/master/doc/protocol.txt
|
||||
udp 11211
|
||||
"\0\x01\0\0\0\x01\0\0version\r\n"
|
||||
|
||||
# Steam, typically using a port in 27015-27030. Send a "Source Engine query"
|
||||
udp 27015-27030
|
||||
"\xff\xff\xff\xffTSource Engine Query\x00"
|
||||
|
||||
# TRIN00 UNIX PING
|
||||
udp 27444 "png l44adsl"
|
||||
|
||||
# BO PING
|
||||
udp 31337
|
||||
"\xce\x63\xd1\xd2\x16\xe7\x13\xcf\x38"
|
||||
"\xa5\xa5\x86\xb2\x75\x4b\x99\xaa\x32\x58"
|
||||
|
||||
# TRIN00 WIN PING
|
||||
udp 34555 "png []..Ks"
|
||||
|
||||
# Beckhoff ADS discovery request
|
||||
# https://github.com/ONE75/adsclient/blob/master/src/AdsClient.Finder/DeviceFinder.cs#L49-L64
|
||||
udp 48899
|
||||
"\x03\x66\x14\x71\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x01\x10\x27\x00\x00\x00\x00"
|
||||
@@ -0,0 +1,20 @@
|
||||
FROM rust:latest
|
||||
|
||||
RUN git clone https://github.com/brandonskerritt/RustScan
|
||||
WORKDIR "/RustScan"
|
||||
RUN git pull --force
|
||||
RUN cargo install cargo-deb
|
||||
|
||||
RUN apt update -y && apt upgrade -y
|
||||
RUN apt install libc6-dev-i386 -y
|
||||
RUN git clone --depth=1 https://github.com/raspberrypi/tools /raspberrypi-tools
|
||||
ENV PATH=/raspberrypi-tools/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian-x64/bin/:$PATH
|
||||
ENV CARGO_TARGET_ARM_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc
|
||||
RUN mkdir /root/.cargo
|
||||
RUN echo "[target.arm-unknown-linux-gnueabihf]" >> /root/.cargo/config
|
||||
RUN echo "strip = { path = \"arm-linux-gnueabihf-strip\" }" >> /root/.cargo/config
|
||||
RUN echo "objcopy = { path = \"arm-linux-gnueabihf-objcopy\" }" >> /root/.cargo/config
|
||||
|
||||
COPY ./entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd /RustScan
|
||||
git pull --force
|
||||
|
||||
#amd64
|
||||
cargo deb
|
||||
|
||||
#arm64
|
||||
rustup target add arm-unknown-linux-gnueabihf
|
||||
cargo deb --target=arm-unknown-linux-gnueabihf
|
||||
|
||||
#i386
|
||||
rustup target add i686-unknown-linux-gnu
|
||||
cargo deb --target=i686-unknown-linux-gnu
|
||||
|
||||
find target/ -name \*.deb -exec cp {} /debs \;
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
docker build -t rustscan-builder . || exit
|
||||
|
||||
# This creates a volume which binds your currentdirectory/debs to
|
||||
# the location where the deb files get spat out in the container.
|
||||
# You don't need to worry about it. Just chmod +x run.sh && ./run.sh and
|
||||
# you'll get yer .deb file in a few minutes. It runs faster after you've used it the first time.
|
||||
docker run -v "$(pwd)/debs:/debs" rustscan-builder
|
||||
-345
@@ -1,345 +0,0 @@
|
||||
//! Provides functions to parse input IP addresses, CIDRs or files.
|
||||
use std::collections::BTreeSet;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{prelude::*, BufReader};
|
||||
use std::net::{IpAddr, SocketAddr, ToSocketAddrs};
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
use cidr_utils::cidr::{IpCidr, IpInet};
|
||||
use hickory_resolver::{
|
||||
config::{NameServerConfig, Protocol, ResolverConfig, ResolverOpts},
|
||||
Resolver,
|
||||
};
|
||||
use log::debug;
|
||||
|
||||
use crate::input::Opts;
|
||||
use crate::warning;
|
||||
|
||||
/// Parses the string(s) into IP addresses.
|
||||
///
|
||||
/// Goes through all possible IP inputs (files or via argparsing).
|
||||
///
|
||||
/// ```rust
|
||||
/// # use rustscan::input::Opts;
|
||||
/// # use rustscan::address::parse_addresses;
|
||||
/// let mut opts = Opts::default();
|
||||
/// opts.addresses = vec!["192.168.0.0/30".to_owned()];
|
||||
///
|
||||
/// let ips = parse_addresses(&opts);
|
||||
/// ```
|
||||
///
|
||||
/// Finally, any duplicates are removed to avoid excessive scans.
|
||||
pub fn parse_addresses(input: &Opts) -> Vec<IpAddr> {
|
||||
let mut ips: Vec<IpAddr> = Vec::new();
|
||||
let mut unresolved_addresses: Vec<&str> = Vec::new();
|
||||
let backup_resolver = get_resolver(&input.resolver);
|
||||
|
||||
for address in &input.addresses {
|
||||
let parsed_ips = parse_address(address, &backup_resolver);
|
||||
if !parsed_ips.is_empty() {
|
||||
ips.extend(parsed_ips);
|
||||
} else {
|
||||
unresolved_addresses.push(address);
|
||||
}
|
||||
}
|
||||
|
||||
// If we got to this point this can only be a file path or the wrong input.
|
||||
for file_path in unresolved_addresses {
|
||||
let file_path = Path::new(file_path);
|
||||
|
||||
if !file_path.is_file() {
|
||||
warning!(
|
||||
format!("Host {file_path:?} could not be resolved."),
|
||||
input.greppable,
|
||||
input.accessible
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Ok(x) = read_ips_from_file(file_path, &backup_resolver) {
|
||||
ips.extend(x);
|
||||
} else {
|
||||
warning!(
|
||||
format!("Host {file_path:?} could not be resolved."),
|
||||
input.greppable,
|
||||
input.accessible
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let excluded_cidrs = parse_excluded_networks(&input.exclude_addresses, &backup_resolver);
|
||||
|
||||
// Remove duplicated/excluded IPs.
|
||||
let mut seen = BTreeSet::new();
|
||||
ips.retain(|ip| seen.insert(*ip) && !excluded_cidrs.iter().any(|cidr| cidr.contains(ip)));
|
||||
|
||||
ips
|
||||
}
|
||||
|
||||
/// Given a string, parse it as a host, IP address, or CIDR.
|
||||
///
|
||||
/// This allows us to pass files as hosts or cidr or IPs easily
|
||||
/// Call this every time you have a possible IP-or-host.
|
||||
///
|
||||
/// If the address is a domain, we can self-resolve the domain locally
|
||||
/// or resolve it by dns resolver list.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use rustscan::address::parse_address;
|
||||
/// # use hickory_resolver::Resolver;
|
||||
/// let ips = parse_address("127.0.0.1", &Resolver::default().unwrap());
|
||||
/// ```
|
||||
pub fn parse_address(address: &str, resolver: &Resolver) -> Vec<IpAddr> {
|
||||
if let Ok(addr) = IpAddr::from_str(address) {
|
||||
// `address` is an IP string
|
||||
vec![addr]
|
||||
} else if let Ok(net_addr) = IpInet::from_str(address) {
|
||||
// `address` is a CIDR string
|
||||
net_addr.network().into_iter().addresses().collect()
|
||||
} else {
|
||||
// `address` is a hostname or DNS name
|
||||
// attempt default DNS lookup
|
||||
match format!("{address}:80").to_socket_addrs() {
|
||||
Ok(mut iter) => vec![iter.next().unwrap().ip()],
|
||||
// default lookup didn't work, so try again with the dedicated resolver
|
||||
Err(_) => resolve_ips_from_host(address, resolver),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Uses DNS to get the IPS associated with host
|
||||
fn resolve_ips_from_host(source: &str, backup_resolver: &Resolver) -> Vec<IpAddr> {
|
||||
let mut ips: Vec<IpAddr> = Vec::new();
|
||||
|
||||
if let Ok(addrs) = source.to_socket_addrs() {
|
||||
for ip in addrs {
|
||||
ips.push(ip.ip());
|
||||
}
|
||||
} else if let Ok(addrs) = backup_resolver.lookup_ip(source) {
|
||||
ips.extend(addrs.iter());
|
||||
}
|
||||
|
||||
ips
|
||||
}
|
||||
|
||||
/// Parses excluded networks from a list of addresses.
|
||||
///
|
||||
/// This function handles three types of inputs:
|
||||
/// 1. CIDR notation (e.g. "192.168.0.0/24")
|
||||
/// 2. Single IP addresses (e.g. "192.168.0.1")
|
||||
/// 3. Hostnames that need to be resolved (e.g. "example.com")
|
||||
///
|
||||
/// ```rust
|
||||
/// # use rustscan::address::parse_excluded_networks;
|
||||
/// # use hickory_resolver::Resolver;
|
||||
/// let resolver = Resolver::default().unwrap();
|
||||
/// let excluded = parse_excluded_networks(&Some(vec!["192.168.0.0/24".to_owned()]), &resolver);
|
||||
/// ```
|
||||
pub fn parse_excluded_networks(
|
||||
exclude_addresses: &Option<Vec<String>>,
|
||||
resolver: &Resolver,
|
||||
) -> Vec<IpCidr> {
|
||||
exclude_addresses
|
||||
.iter()
|
||||
.flatten()
|
||||
.flat_map(|addr| parse_single_excluded_address(addr, resolver))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Parses a single address into an IpCidr, handling CIDR notation, IP addresses, and hostnames.
|
||||
fn parse_single_excluded_address(addr: &str, resolver: &Resolver) -> Vec<IpCidr> {
|
||||
if let Ok(cidr) = IpCidr::from_str(addr) {
|
||||
return vec![cidr];
|
||||
}
|
||||
|
||||
if let Ok(ip) = IpAddr::from_str(addr) {
|
||||
return vec![IpCidr::new_host(ip)];
|
||||
}
|
||||
|
||||
resolve_ips_from_host(addr, resolver)
|
||||
.into_iter()
|
||||
.map(IpCidr::new_host)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Derive a DNS resolver.
|
||||
///
|
||||
/// 1. if the `resolver` parameter has been set:
|
||||
/// 1. assume the parameter is a path and attempt to read IPs.
|
||||
/// 2. parse the input as a comma-separated list of IPs.
|
||||
/// 2. if `resolver` is not set:
|
||||
/// 1. attempt to derive a resolver from the system config. (e.g.
|
||||
/// `/etc/resolv.conf` on *nix).
|
||||
/// 2. finally, build a CloudFlare-based resolver (default
|
||||
/// behaviour).
|
||||
fn get_resolver(resolver: &Option<String>) -> Resolver {
|
||||
match resolver {
|
||||
Some(r) => {
|
||||
let mut config = ResolverConfig::new();
|
||||
let resolver_ips = match read_resolver_from_file(r) {
|
||||
Ok(ips) => ips,
|
||||
Err(_) => r
|
||||
.split(',')
|
||||
.filter_map(|r| IpAddr::from_str(r).ok())
|
||||
.collect::<Vec<_>>(),
|
||||
};
|
||||
for ip in resolver_ips {
|
||||
config.add_name_server(NameServerConfig::new(
|
||||
SocketAddr::new(ip, 53),
|
||||
Protocol::Udp,
|
||||
));
|
||||
}
|
||||
Resolver::new(config, ResolverOpts::default()).unwrap()
|
||||
}
|
||||
None => match Resolver::from_system_conf() {
|
||||
Ok(resolver) => resolver,
|
||||
Err(_) => {
|
||||
Resolver::new(ResolverConfig::cloudflare_tls(), ResolverOpts::default()).unwrap()
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses and input file of IPs for use in DNS resolution.
|
||||
fn read_resolver_from_file(path: &str) -> Result<Vec<IpAddr>, std::io::Error> {
|
||||
let ips = fs::read_to_string(path)?
|
||||
.lines()
|
||||
.filter_map(|line| IpAddr::from_str(line.trim()).ok())
|
||||
.collect();
|
||||
|
||||
Ok(ips)
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
/// Parses an input file of IPs and uses those
|
||||
fn read_ips_from_file(
|
||||
ips: &std::path::Path,
|
||||
backup_resolver: &Resolver,
|
||||
) -> Result<Vec<IpAddr>, std::io::Error> {
|
||||
let file = File::open(ips)?;
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
let mut ips: Vec<IpAddr> = Vec::new();
|
||||
|
||||
for address_line in reader.lines() {
|
||||
if let Ok(address) = address_line {
|
||||
ips.extend(parse_address(&address, backup_resolver));
|
||||
} else {
|
||||
debug!("Line in file is not valid");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ips)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{parse_addresses, Opts};
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
#[test]
|
||||
fn parse_correct_addresses() {
|
||||
let opts = Opts {
|
||||
addresses: vec!["127.0.0.1".to_owned(), "192.168.0.0/30".to_owned()],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let ips = parse_addresses(&opts);
|
||||
|
||||
assert_eq!(
|
||||
ips,
|
||||
[
|
||||
Ipv4Addr::new(127, 0, 0, 1),
|
||||
Ipv4Addr::new(192, 168, 0, 0),
|
||||
Ipv4Addr::new(192, 168, 0, 1),
|
||||
Ipv4Addr::new(192, 168, 0, 2),
|
||||
Ipv4Addr::new(192, 168, 0, 3)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_addresses_with_address_exclusions() {
|
||||
let opts = Opts {
|
||||
addresses: vec!["192.168.0.0/30".to_owned()],
|
||||
exclude_addresses: Some(vec!["192.168.0.1".to_owned()]),
|
||||
..Default::default()
|
||||
};
|
||||
let ips = parse_addresses(&opts);
|
||||
|
||||
assert_eq!(
|
||||
ips,
|
||||
[
|
||||
Ipv4Addr::new(192, 168, 0, 0),
|
||||
Ipv4Addr::new(192, 168, 0, 2),
|
||||
Ipv4Addr::new(192, 168, 0, 3)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_addresses_with_cidr_exclusions() {
|
||||
let opts = Opts {
|
||||
addresses: vec!["192.168.0.0/29".to_owned()],
|
||||
exclude_addresses: Some(vec!["192.168.0.0/30".to_owned()]),
|
||||
..Default::default()
|
||||
};
|
||||
let ips = parse_addresses(&opts);
|
||||
|
||||
assert_eq!(
|
||||
ips,
|
||||
[
|
||||
Ipv4Addr::new(192, 168, 0, 4),
|
||||
Ipv4Addr::new(192, 168, 0, 5),
|
||||
Ipv4Addr::new(192, 168, 0, 6),
|
||||
Ipv4Addr::new(192, 168, 0, 7),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_addresses_with_incorrect_address_exclusions() {
|
||||
let opts = Opts {
|
||||
addresses: vec!["192.168.0.0/30".to_owned()],
|
||||
exclude_addresses: Some(vec!["192.168.0.1".to_owned()]),
|
||||
..Default::default()
|
||||
};
|
||||
let ips = parse_addresses(&opts);
|
||||
|
||||
assert_eq!(
|
||||
ips,
|
||||
[
|
||||
Ipv4Addr::new(192, 168, 0, 0),
|
||||
Ipv4Addr::new(192, 168, 0, 2),
|
||||
Ipv4Addr::new(192, 168, 0, 3)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_duplicate_cidrs() {
|
||||
let opts = Opts {
|
||||
addresses: vec!["79.98.104.0/21".to_owned(), "79.98.104.0/24".to_owned()],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let ips = parse_addresses(&opts);
|
||||
|
||||
assert_eq!(ips.len(), 2_048);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_overspecific_cidr() {
|
||||
// a canonical CIDR string has 0 in all host bits, but we want to treat any CIDR-like string as CIDR
|
||||
let opts = Opts {
|
||||
addresses: vec!["192.128.1.1/24".to_owned()],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let ips = parse_addresses(&opts);
|
||||
|
||||
assert_eq!(ips.len(), 256);
|
||||
}
|
||||
}
|
||||
+10
-13
@@ -1,20 +1,15 @@
|
||||
//! Provides functionality to capture timing information for scans.
|
||||
//!
|
||||
//! # Usage
|
||||
//!
|
||||
//! ```rust
|
||||
//! ```
|
||||
//! // Initiate Benchmark vector
|
||||
//! # use rustscan::benchmark::{Benchmark, NamedTimer};
|
||||
//! # use log::info;
|
||||
//! let mut bm = Benchmark::init();
|
||||
//! // Start named timer with name
|
||||
//! let mut example_bench = NamedTimer::start("Example Bench");
|
||||
//! // Stop named timer
|
||||
//! example_bench.end();
|
||||
//! example_bench.stop();
|
||||
//! // Add named timer to Benchmarks
|
||||
//! bm.push(example_bench);
|
||||
//! // Print Benchmark Summary
|
||||
//! info!("{}", bm.summary());
|
||||
//! info!({}, bm.summary());
|
||||
//! ```
|
||||
use std::time::Instant;
|
||||
|
||||
@@ -42,11 +37,13 @@ impl Benchmark {
|
||||
let mut summary = String::from("\nRustScan Benchmark Summary");
|
||||
|
||||
for timer in &self.named_timers {
|
||||
if let Some(start) = timer.start {
|
||||
if let Some(end) = timer.end {
|
||||
let runtime_secs = end.saturating_duration_since(start).as_secs_f32();
|
||||
summary.push_str(&format!("\n{0: <10} | {1: <10}s", timer.name, runtime_secs));
|
||||
}
|
||||
if timer.start.is_some() && timer.end.is_some() {
|
||||
let runtime_secs = timer
|
||||
.end
|
||||
.unwrap()
|
||||
.saturating_duration_since(timer.start.unwrap())
|
||||
.as_secs_f32();
|
||||
summary.push_str(&format!("\n{0: <10} | {1: <10}s", timer.name, runtime_secs));
|
||||
}
|
||||
}
|
||||
summary
|
||||
|
||||
-445
@@ -1,446 +1 @@
|
||||
//! Provides a means to read, parse and hold configuration options for scans.
|
||||
use clap::{Parser, ValueEnum};
|
||||
use serde_derive::Deserialize;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const LOWEST_PORT_NUMBER: u16 = 1;
|
||||
const TOP_PORT_NUMBER: u16 = 65535;
|
||||
|
||||
/// Represents the strategy in which the port scanning will run.
|
||||
/// - Serial will run from start to end, for example 1 to 1_000.
|
||||
/// - Random will randomize the order in which ports will be scanned.
|
||||
#[derive(Deserialize, Debug, ValueEnum, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ScanOrder {
|
||||
Serial,
|
||||
Random,
|
||||
}
|
||||
|
||||
/// Represents the scripts variant.
|
||||
/// - none will avoid running any script, only portscan results will be shown.
|
||||
/// - default will run the default embedded nmap script, that's part of RustScan since the beginning.
|
||||
/// - custom will read the ScriptConfig file and the available scripts in the predefined folders
|
||||
#[derive(Deserialize, Debug, ValueEnum, Clone, PartialEq, Eq, Copy)]
|
||||
pub enum ScriptsRequired {
|
||||
None,
|
||||
Default,
|
||||
Custom,
|
||||
}
|
||||
|
||||
/// Represents the range of ports to be scanned.
|
||||
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PortRange {
|
||||
pub start: u16,
|
||||
pub end: u16,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
fn parse_range(input: &str) -> Result<PortRange, String> {
|
||||
let range = input
|
||||
.split('-')
|
||||
.map(str::parse)
|
||||
.collect::<Result<Vec<u16>, std::num::ParseIntError>>();
|
||||
|
||||
if range.is_err() {
|
||||
return Err(String::from(
|
||||
"the range format must be 'start-end'. Example: 1-1000.",
|
||||
));
|
||||
}
|
||||
|
||||
match range.unwrap().as_slice() {
|
||||
[start, end] => Ok(PortRange {
|
||||
start: *start,
|
||||
end: *end,
|
||||
}),
|
||||
_ => Err(String::from(
|
||||
"the range format must be 'start-end'. Example: 1-1000.",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
#[command(
|
||||
name = "rustscan",
|
||||
version = env!("CARGO_PKG_VERSION"),
|
||||
max_term_width = 120,
|
||||
help_template = "{bin} {version}\n{about}\n\nUSAGE:\n {usage}\n\nOPTIONS:\n{options}",
|
||||
)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
/// Fast Port Scanner built in Rust.
|
||||
/// WARNING Do not use this program against sensitive infrastructure since the
|
||||
/// specified server may not be able to handle this many socket connections at once.
|
||||
/// - Discord <http://discord.skerritt.blog>
|
||||
/// - GitHub <https://github.com/RustScan/RustScan>
|
||||
pub struct Opts {
|
||||
/// A comma-delimited list or newline-delimited file of separated CIDRs, IPs, or hosts to be scanned.
|
||||
#[arg(short, long, value_delimiter = ',')]
|
||||
pub addresses: Vec<String>,
|
||||
|
||||
/// A list of comma separated ports to be scanned. Example: 80,443,8080.
|
||||
#[arg(short, long, value_delimiter = ',')]
|
||||
pub ports: Option<Vec<u16>>,
|
||||
|
||||
/// A range of ports with format start-end. Example: 1-1000.
|
||||
#[arg(short, long, conflicts_with = "ports", value_parser = parse_range)]
|
||||
pub range: Option<PortRange>,
|
||||
|
||||
/// Whether to ignore the configuration file or not.
|
||||
#[arg(short, long)]
|
||||
pub no_config: bool,
|
||||
|
||||
/// Hide the banner
|
||||
#[arg(long)]
|
||||
pub no_banner: bool,
|
||||
|
||||
/// Custom path to config file
|
||||
#[arg(short, long, value_parser)]
|
||||
pub config_path: Option<PathBuf>,
|
||||
|
||||
/// Greppable mode. Only output the ports. No Nmap. Useful for grep or outputting to a file.
|
||||
#[arg(short, long)]
|
||||
pub greppable: bool,
|
||||
|
||||
/// Accessible mode. Turns off features which negatively affect screen readers.
|
||||
#[arg(long)]
|
||||
pub accessible: bool,
|
||||
|
||||
/// A comma-delimited list or file of DNS resolvers.
|
||||
#[arg(long)]
|
||||
pub resolver: Option<String>,
|
||||
|
||||
/// The batch size for port scanning, it increases or slows the speed of
|
||||
/// scanning. Depends on the open file limit of your OS. If you do 65535
|
||||
/// it will do every port at the same time. Although, your OS may not
|
||||
/// support this.
|
||||
#[arg(short, long, default_value = "4500")]
|
||||
pub batch_size: usize,
|
||||
|
||||
/// The timeout in milliseconds before a port is assumed to be closed.
|
||||
#[arg(short, long, default_value = "1500")]
|
||||
pub timeout: u32,
|
||||
|
||||
/// The number of tries before a port is assumed to be closed.
|
||||
/// If set to 0, rustscan will correct it to 1.
|
||||
#[arg(long, default_value = "1")]
|
||||
pub tries: u8,
|
||||
|
||||
/// Automatically ups the ULIMIT with the value you provided.
|
||||
#[arg(short, long)]
|
||||
pub ulimit: Option<usize>,
|
||||
|
||||
/// The order of scanning to be performed. The "serial" option will
|
||||
/// scan ports in ascending order while the "random" option will scan
|
||||
/// ports randomly.
|
||||
#[arg(long, value_enum, ignore_case = true, default_value = "serial")]
|
||||
pub scan_order: ScanOrder,
|
||||
|
||||
/// Level of scripting required for the run.
|
||||
#[arg(long, value_enum, ignore_case = true, default_value = "default")]
|
||||
pub scripts: ScriptsRequired,
|
||||
|
||||
/// Use the top 1000 ports.
|
||||
#[arg(long)]
|
||||
pub top: bool,
|
||||
|
||||
/// The Script arguments to run.
|
||||
/// To use the argument -A, end RustScan's args with '-- -A'.
|
||||
/// Example: 'rustscan -t 1500 -a 127.0.0.1 -- -A -sC'.
|
||||
/// This command adds -Pn -vvv -p $PORTS automatically to nmap.
|
||||
/// For things like --script '(safe and vuln)' enclose it in quotations marks \"'(safe and vuln)'\"
|
||||
#[arg(last = true)]
|
||||
pub command: Vec<String>,
|
||||
|
||||
/// A list of comma separated ports to be excluded from scanning. Example: 80,443,8080.
|
||||
#[arg(short, long, value_delimiter = ',')]
|
||||
pub exclude_ports: Option<Vec<u16>>,
|
||||
|
||||
/// A list of comma separated CIDRs, IPs, or hosts to be excluded from scanning.
|
||||
#[arg(short = 'x', long = "exclude-addresses", value_delimiter = ',')]
|
||||
pub exclude_addresses: Option<Vec<String>>,
|
||||
|
||||
/// UDP scanning mode, finds UDP ports that send back responses
|
||||
#[arg(long)]
|
||||
pub udp: bool,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl Opts {
|
||||
pub fn read() -> Self {
|
||||
let mut opts = Opts::parse();
|
||||
|
||||
if opts.ports.is_none() && opts.range.is_none() {
|
||||
opts.range = Some(PortRange {
|
||||
start: LOWEST_PORT_NUMBER,
|
||||
end: TOP_PORT_NUMBER,
|
||||
});
|
||||
}
|
||||
|
||||
opts
|
||||
}
|
||||
|
||||
/// Reads the command line arguments into an Opts struct and merge
|
||||
/// values found within the user configuration file.
|
||||
pub fn merge(&mut self, config: &Config) {
|
||||
if !self.no_config {
|
||||
self.merge_required(config);
|
||||
self.merge_optional(config);
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_required(&mut self, config: &Config) {
|
||||
macro_rules! merge_required {
|
||||
($($field: ident),+) => {
|
||||
$(
|
||||
if let Some(e) = &config.$field {
|
||||
self.$field = e.clone();
|
||||
}
|
||||
)+
|
||||
}
|
||||
}
|
||||
|
||||
merge_required!(
|
||||
addresses, greppable, accessible, batch_size, timeout, tries, scan_order, scripts,
|
||||
command, udp, no_banner
|
||||
);
|
||||
}
|
||||
|
||||
fn merge_optional(&mut self, config: &Config) {
|
||||
macro_rules! merge_optional {
|
||||
($($field: ident),+) => {
|
||||
$(
|
||||
if config.$field.is_some() {
|
||||
self.$field = config.$field.clone();
|
||||
}
|
||||
)+
|
||||
}
|
||||
}
|
||||
|
||||
// Only use top ports when the user asks for them
|
||||
if self.top && config.ports.is_some() {
|
||||
self.ports = config.ports.clone();
|
||||
}
|
||||
|
||||
merge_optional!(range, resolver, ulimit, exclude_ports, exclude_addresses);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Opts {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
addresses: vec![],
|
||||
ports: None,
|
||||
range: None,
|
||||
greppable: true,
|
||||
batch_size: 0,
|
||||
timeout: 0,
|
||||
tries: 0,
|
||||
ulimit: None,
|
||||
command: vec![],
|
||||
accessible: false,
|
||||
resolver: None,
|
||||
scan_order: ScanOrder::Serial,
|
||||
no_config: true,
|
||||
no_banner: false,
|
||||
top: false,
|
||||
scripts: ScriptsRequired::Default,
|
||||
config_path: None,
|
||||
exclude_ports: None,
|
||||
exclude_addresses: None,
|
||||
udp: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Struct used to deserialize the options specified within our config file.
|
||||
/// These will be further merged with our command line arguments in order to
|
||||
/// generate the final Opts struct.
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Config {
|
||||
addresses: Option<Vec<String>>,
|
||||
ports: Option<Vec<u16>>,
|
||||
range: Option<PortRange>,
|
||||
greppable: Option<bool>,
|
||||
accessible: Option<bool>,
|
||||
batch_size: Option<usize>,
|
||||
timeout: Option<u32>,
|
||||
tries: Option<u8>,
|
||||
ulimit: Option<usize>,
|
||||
resolver: Option<String>,
|
||||
scan_order: Option<ScanOrder>,
|
||||
command: Option<Vec<String>>,
|
||||
scripts: Option<ScriptsRequired>,
|
||||
exclude_ports: Option<Vec<u16>>,
|
||||
exclude_addresses: Option<Vec<String>>,
|
||||
udp: Option<bool>,
|
||||
no_banner: Option<bool>,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
#[allow(clippy::doc_link_with_quotes)]
|
||||
#[allow(clippy::manual_unwrap_or_default)]
|
||||
impl Config {
|
||||
/// Reads the configuration file with TOML format and parses it into a
|
||||
/// Config struct.
|
||||
///
|
||||
/// # Format
|
||||
///
|
||||
/// addresses = ["127.0.0.1", "127.0.0.1"]
|
||||
/// ports = [80, 443, 8080]
|
||||
/// greppable = true
|
||||
/// scan_order = "Serial"
|
||||
/// exclude_ports = [8080, 9090, 80]
|
||||
/// udp = false
|
||||
///
|
||||
pub fn read(custom_config_path: Option<PathBuf>) -> Self {
|
||||
let mut content = String::new();
|
||||
let config_path = custom_config_path.unwrap_or_else(|| {
|
||||
let path = default_config_path();
|
||||
match path.exists() {
|
||||
true => path,
|
||||
false => old_default_config_path(),
|
||||
}
|
||||
});
|
||||
|
||||
if config_path.exists() {
|
||||
content = match fs::read_to_string(config_path) {
|
||||
Ok(content) => content,
|
||||
Err(_) => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
let config: Config = match toml::from_str(&content) {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
println!("Found {e} in configuration file.\nAborting scan.\n");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
config
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs default path to config toml
|
||||
pub fn default_config_path() -> PathBuf {
|
||||
let Some(mut config_path) = dirs::config_dir() else {
|
||||
panic!("Could not infer config file path.");
|
||||
};
|
||||
config_path.push(".rustscan.toml");
|
||||
config_path
|
||||
}
|
||||
|
||||
/// Returns the deprecated home directory config path used for backwards compatibility.
|
||||
pub fn old_default_config_path() -> PathBuf {
|
||||
let Some(mut config_path) = dirs::home_dir() else {
|
||||
panic!("Could not infer config file path.");
|
||||
};
|
||||
config_path.push(".rustscan.toml");
|
||||
config_path
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use clap::{CommandFactory, Parser};
|
||||
use parameterized::parameterized;
|
||||
|
||||
use super::{Config, Opts, PortRange, ScanOrder, ScriptsRequired};
|
||||
|
||||
impl Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
addresses: Some(vec!["127.0.0.1".to_owned()]),
|
||||
ports: None,
|
||||
range: None,
|
||||
greppable: Some(true),
|
||||
batch_size: Some(25_000),
|
||||
timeout: Some(1_000),
|
||||
tries: Some(1),
|
||||
ulimit: None,
|
||||
command: Some(vec!["-A".to_owned()]),
|
||||
accessible: Some(true),
|
||||
resolver: None,
|
||||
scan_order: Some(ScanOrder::Random),
|
||||
scripts: None,
|
||||
exclude_ports: None,
|
||||
exclude_addresses: None,
|
||||
udp: Some(false),
|
||||
no_banner: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_cli() {
|
||||
Opts::command().debug_assert();
|
||||
}
|
||||
|
||||
#[parameterized(input = {
|
||||
vec!["rustscan", "--addresses", "127.0.0.1"],
|
||||
vec!["rustscan", "--addresses", "127.0.0.1", "--", "-sCV"],
|
||||
vec!["rustscan", "--addresses", "127.0.0.1", "--", "-A"],
|
||||
vec!["rustscan", "-t", "1500", "-a", "127.0.0.1", "--", "-A", "-sC"],
|
||||
vec!["rustscan", "--addresses", "127.0.0.1", "--", "--script", r#""'(safe and vuln)'""#],
|
||||
}, command = {
|
||||
vec![],
|
||||
vec!["-sCV".to_owned()],
|
||||
vec!["-A".to_owned()],
|
||||
vec!["-A".to_owned(), "-sC".to_owned()],
|
||||
vec!["--script".to_owned(), "\"'(safe and vuln)'\"".to_owned()],
|
||||
})]
|
||||
fn parse_trailing_command(input: Vec<&str>, command: Vec<String>) {
|
||||
let opts = Opts::parse_from(input);
|
||||
|
||||
assert_eq!(vec!["127.0.0.1".to_owned()], opts.addresses);
|
||||
assert_eq!(command, opts.command);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opts_no_merge_when_config_is_ignored() {
|
||||
let mut opts = Opts::default();
|
||||
let config = Config::default();
|
||||
|
||||
opts.merge(&config);
|
||||
|
||||
assert_eq!(opts.addresses, vec![] as Vec<String>);
|
||||
assert!(opts.greppable);
|
||||
assert!(!opts.accessible);
|
||||
assert_eq!(opts.timeout, 0);
|
||||
assert_eq!(opts.command, vec![] as Vec<String>);
|
||||
assert_eq!(opts.scan_order, ScanOrder::Serial);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opts_merge_required_arguments() {
|
||||
let mut opts = Opts::default();
|
||||
let config = Config::default();
|
||||
|
||||
opts.merge_required(&config);
|
||||
|
||||
assert_eq!(opts.addresses, config.addresses.unwrap());
|
||||
assert_eq!(opts.greppable, config.greppable.unwrap());
|
||||
assert_eq!(opts.timeout, config.timeout.unwrap());
|
||||
assert_eq!(opts.command, config.command.unwrap());
|
||||
assert_eq!(opts.accessible, config.accessible.unwrap());
|
||||
assert_eq!(opts.scan_order, config.scan_order.unwrap());
|
||||
assert_eq!(opts.scripts, ScriptsRequired::Default);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opts_merge_optional_arguments() {
|
||||
let mut opts = Opts::default();
|
||||
let mut config = Config::default();
|
||||
config.range = Some(PortRange {
|
||||
start: 1,
|
||||
end: 1_000,
|
||||
});
|
||||
config.ulimit = Some(1_000);
|
||||
config.resolver = Some("1.1.1.1".to_owned());
|
||||
|
||||
opts.merge_optional(&config);
|
||||
|
||||
assert_eq!(opts.range, config.range);
|
||||
assert_eq!(opts.ulimit, config.ulimit);
|
||||
assert_eq!(opts.resolver, config.resolver);
|
||||
}
|
||||
}
|
||||
-60
@@ -1,60 +0,0 @@
|
||||
//! This crate exposes the internal functionality of the
|
||||
//! [RustScan](https://rustscan.github.io/RustScan) port scanner.
|
||||
//!
|
||||
//! ## Example: perform a scan against localhost
|
||||
//!
|
||||
//! The core scanning behaviour is managed by
|
||||
//! [`Scanner`](crate::scanner::Scanner) which in turn requires a
|
||||
//! [`PortStrategy`](crate::port_strategy::PortStrategy):
|
||||
//!
|
||||
//! This example is compiled but never executed by the test suite.
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use async_std::task::block_on;
|
||||
//! use std::{net::IpAddr, time::Duration};
|
||||
//!
|
||||
//! use rustscan::input::{PortRange, ScanOrder};
|
||||
//! use rustscan::port_strategy::PortStrategy;
|
||||
//! use rustscan::scanner::Scanner;
|
||||
//!
|
||||
//! fn main() {
|
||||
//! let addrs = vec!["127.0.0.1".parse::<IpAddr>().unwrap()];
|
||||
//! let range = PortRange {
|
||||
//! start: 1,
|
||||
//! end: 1_000,
|
||||
//! };
|
||||
//! let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random); // can be serial, random or manual https://github.com/RustScan/RustScan/blob/master/src/port_strategy/mod.rs
|
||||
//! let scanner = Scanner::new(
|
||||
//! &addrs, // the addresses to scan
|
||||
//! 10, // batch_size is how many ports at a time should be scanned
|
||||
//! Duration::from_millis(100), //T imeout is the time RustScan should wait before declaring a port closed. As datatype Duration.
|
||||
//! 1, // Tries, how many retries should RustScan do?
|
||||
//! true, // greppable is whether or not RustScan should print things, or wait until the end to print only the ip
|
||||
//! strategy, // the port strategy used
|
||||
//! true, // accessible, should the output be A11Y compliant?
|
||||
//! vec![9000], // What ports should RustScan exclude?
|
||||
//! false, // is this a UDP scan?
|
||||
//! );
|
||||
//!
|
||||
//! let scan_result = block_on(scanner.run());
|
||||
//!
|
||||
//! println!("{:?}", scan_result);
|
||||
//! }
|
||||
//! ```
|
||||
#![allow(clippy::needless_doctest_main)]
|
||||
|
||||
pub mod tui;
|
||||
|
||||
pub mod input;
|
||||
|
||||
pub mod scanner;
|
||||
|
||||
pub mod port_strategy;
|
||||
|
||||
pub mod benchmark;
|
||||
|
||||
pub mod scripts;
|
||||
|
||||
pub mod address;
|
||||
|
||||
pub mod generated;
|
||||
+257
-90
@@ -2,30 +2,48 @@
|
||||
#![warn(clippy::pedantic)]
|
||||
#![allow(clippy::doc_markdown, clippy::if_not_else, clippy::non_ascii_literal)]
|
||||
|
||||
use rustscan::benchmark::{Benchmark, NamedTimer};
|
||||
use rustscan::input::{self, Config, Opts, ScriptsRequired};
|
||||
use rustscan::port_strategy::PortStrategy;
|
||||
use rustscan::scanner::Scanner;
|
||||
use rustscan::scripts::{init_scripts, Script, ScriptFile};
|
||||
use rustscan::{detail, funny_opening, output, warning};
|
||||
extern crate shell_words;
|
||||
|
||||
mod tui;
|
||||
|
||||
mod input;
|
||||
use input::{Config, Opts, PortRange, ScanOrder, ScriptsRequired};
|
||||
|
||||
mod scanner;
|
||||
use scanner::Scanner;
|
||||
|
||||
mod port_strategy;
|
||||
use port_strategy::PortStrategy;
|
||||
|
||||
mod benchmark;
|
||||
use benchmark::{Benchmark, NamedTimer};
|
||||
|
||||
mod scripts;
|
||||
use scripts::{init_scripts, Script, ScriptFile};
|
||||
|
||||
use cidr_utils::cidr::IpCidr;
|
||||
use colorful::{Color, Colorful};
|
||||
use futures::executor::block_on;
|
||||
use std::collections::HashMap;
|
||||
use std::net::IpAddr;
|
||||
use std::fs::File;
|
||||
use std::io::{prelude::*, BufReader};
|
||||
use std::net::{IpAddr, ToSocketAddrs};
|
||||
use std::path::Path;
|
||||
use std::string::ToString;
|
||||
use std::time::Duration;
|
||||
|
||||
use rustscan::address::parse_addresses;
|
||||
use trust_dns_resolver::{
|
||||
config::{ResolverConfig, ResolverOpts},
|
||||
Resolver,
|
||||
};
|
||||
|
||||
extern crate colorful;
|
||||
extern crate dirs;
|
||||
|
||||
// Average value for Ubuntu
|
||||
#[cfg(unix)]
|
||||
const DEFAULT_FILE_DESCRIPTORS_LIMIT: usize = 8000;
|
||||
const DEFAULT_FILE_DESCRIPTORS_LIMIT: u64 = 8000;
|
||||
// Safest batch size based on experimentation
|
||||
const AVERAGE_BATCH_SIZE: usize = 3000;
|
||||
const AVERAGE_BATCH_SIZE: u16 = 3000;
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
@@ -35,9 +53,6 @@ extern crate log;
|
||||
/// Faster Nmap scanning with Rust
|
||||
/// If you're looking for the actual scanning, check out the module Scanner
|
||||
fn main() {
|
||||
#[cfg(not(unix))]
|
||||
let _ = ansi_term::enable_ansi_support();
|
||||
|
||||
env_logger::init();
|
||||
let mut benchmarks = Benchmark::init();
|
||||
let mut rustscan_bench = NamedTimer::start("RustScan");
|
||||
@@ -46,9 +61,9 @@ fn main() {
|
||||
let config = Config::read(opts.config_path.clone());
|
||||
opts.merge(&config);
|
||||
|
||||
debug!("Main() `opts` arguments are {opts:?}");
|
||||
debug!("Main() `opts` arguments are {:?}", opts);
|
||||
|
||||
let scripts_to_run: Vec<ScriptFile> = match init_scripts(&opts.scripts) {
|
||||
let scripts_to_run: Vec<ScriptFile> = match init_scripts(opts.scripts) {
|
||||
Ok(scripts_to_run) => scripts_to_run,
|
||||
Err(e) => {
|
||||
warning!(
|
||||
@@ -60,9 +75,9 @@ fn main() {
|
||||
}
|
||||
};
|
||||
|
||||
debug!("Scripts initialized {scripts_to_run:?}");
|
||||
debug!("Scripts initialized {:?}", &scripts_to_run);
|
||||
|
||||
if !opts.greppable && !opts.accessible && !opts.no_banner {
|
||||
if !opts.greppable && !opts.accessible {
|
||||
print_opening(&opts);
|
||||
}
|
||||
|
||||
@@ -78,10 +93,10 @@ fn main() {
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
let batch_size: usize = infer_batch_size(&opts, adjust_ulimit_size(&opts));
|
||||
let batch_size: u16 = infer_batch_size(&opts, adjust_ulimit_size(&opts));
|
||||
|
||||
#[cfg(not(unix))]
|
||||
let batch_size: usize = AVERAGE_BATCH_SIZE;
|
||||
let batch_size: u16 = AVERAGE_BATCH_SIZE;
|
||||
|
||||
let scanner = Scanner::new(
|
||||
&ips,
|
||||
@@ -91,10 +106,8 @@ fn main() {
|
||||
opts.greppable,
|
||||
PortStrategy::pick(&opts.range, opts.ports, opts.scan_order),
|
||||
opts.accessible,
|
||||
opts.exclude_ports.unwrap_or_default(),
|
||||
opts.udp,
|
||||
);
|
||||
debug!("Scanner finished building: {scanner:?}");
|
||||
debug!("Scanner finished building: {:?}", scanner);
|
||||
|
||||
let mut portscan_bench = NamedTimer::start("Portscan");
|
||||
let scan_result = block_on(scanner.run());
|
||||
@@ -136,7 +149,7 @@ fn main() {
|
||||
|
||||
// if option scripts is none, no script will be spawned
|
||||
if opts.greppable || opts.scripts == ScriptsRequired::None {
|
||||
println!("{ip} -> [{ports_str}]");
|
||||
println!("{} -> [{}]", &ip, ports_str);
|
||||
continue;
|
||||
}
|
||||
detail!("Starting Script(s)", opts.greppable, opts.accessible);
|
||||
@@ -146,7 +159,7 @@ fn main() {
|
||||
// This part allows us to add commandline arguments to the Script call_format, appending them to the end of the command.
|
||||
if !opts.command.is_empty() {
|
||||
let user_extra_args = &opts.command.join(" ");
|
||||
debug!("Extra args vec {user_extra_args:?}");
|
||||
debug!("Extra args vec {:?}", user_extra_args);
|
||||
if script_f.call_format.is_some() {
|
||||
let mut call_f = script_f.call_format.unwrap();
|
||||
call_f.push(' ');
|
||||
@@ -156,7 +169,7 @@ fn main() {
|
||||
opts.greppable,
|
||||
opts.accessible
|
||||
);
|
||||
debug!("Call format {call_f}");
|
||||
debug!("Call format {}", call_f);
|
||||
script_f.call_format = Some(call_f);
|
||||
}
|
||||
}
|
||||
@@ -173,7 +186,7 @@ fn main() {
|
||||
);
|
||||
match script.run() {
|
||||
Ok(script_result) => {
|
||||
detail!(script_result.clone(), opts.greppable, opts.accessible);
|
||||
detail!(script_result.to_string(), opts.greppable, opts.accessible);
|
||||
}
|
||||
Err(e) => {
|
||||
warning!(&format!("Error {e}"), opts.greppable, opts.accessible);
|
||||
@@ -187,7 +200,7 @@ fn main() {
|
||||
benchmarks.push(script_bench);
|
||||
rustscan_bench.end();
|
||||
benchmarks.push(rustscan_bench);
|
||||
debug!("Benchmarks raw {benchmarks:?}");
|
||||
debug!("Benchmarks raw {:?}", benchmarks);
|
||||
info!("{}", benchmarks.summary());
|
||||
}
|
||||
|
||||
@@ -195,17 +208,22 @@ fn main() {
|
||||
#[allow(clippy::items_after_statements, clippy::needless_raw_string_hashes)]
|
||||
fn print_opening(opts: &Opts) {
|
||||
debug!("Printing opening");
|
||||
let s = r#".----. .-. .-. .----..---. .----. .---. .--. .-. .-.
|
||||
| {} }| { } |{ {__ {_ _}{ {__ / ___} / {} \ | `| |
|
||||
| .-. \| {_} |.-._} } | | .-._} }\ }/ /\ \| |\ |
|
||||
`-' `-'`-----'`----' `-' `----' `---' `-' `-'`-' `-'
|
||||
The Modern Day Port Scanner."#;
|
||||
|
||||
let s = format!(
|
||||
"{}\n{}\n{}\n{}\n{}",
|
||||
r#".----. .-. .-. .----..---. .----. .---. .--. .-. .-."#,
|
||||
r#"| {} }| { } |{ {__ {_ _}{ {__ / ___} / {} \ | `| |"#,
|
||||
r#"| .-. \| {_} |.-._} } | | .-._} }\ }/ /\ \| |\ |"#,
|
||||
r#"`-' `-'`-----'`----' `-' `----' `---' `-' `-'`-' `-'"#,
|
||||
r#"The Modern Day Port Scanner."#
|
||||
);
|
||||
println!("{}", s.gradient(Color::Green).bold());
|
||||
let info = r#"________________________________________
|
||||
: http://discord.skerritt.blog :
|
||||
: https://github.com/RustScan/RustScan :
|
||||
--------------------------------------"#;
|
||||
let info = format!(
|
||||
"{}\n{}\n{}\n{}",
|
||||
r#"________________________________________"#,
|
||||
r#": http://discord.skerritt.blog :"#,
|
||||
r#": https://github.com/RustScan/RustScan :"#,
|
||||
r#" --------------------------------------"#
|
||||
);
|
||||
println!("{}", info.gradient(Color::Yellow).bold());
|
||||
funny_opening!();
|
||||
|
||||
@@ -219,26 +237,111 @@ The Modern Day Port Scanner."#;
|
||||
opts.greppable,
|
||||
opts.accessible
|
||||
);
|
||||
}
|
||||
|
||||
if opts.config_path.is_none() {
|
||||
let old_config_path = input::old_default_config_path();
|
||||
detail!(
|
||||
format!(
|
||||
"For backwards compatibility, the config file may also be at {old_config_path:?}"
|
||||
),
|
||||
opts.greppable,
|
||||
opts.accessible
|
||||
);
|
||||
/// Goes through all possible IP inputs (files or via argparsing)
|
||||
/// Parses the string(s) into IPs
|
||||
fn parse_addresses(input: &Opts) -> Vec<IpAddr> {
|
||||
let mut ips: Vec<IpAddr> = Vec::new();
|
||||
let mut unresolved_addresses: Vec<&str> = Vec::new();
|
||||
let backup_resolver =
|
||||
Resolver::new(ResolverConfig::cloudflare_tls(), ResolverOpts::default()).unwrap();
|
||||
|
||||
for address in &input.addresses {
|
||||
let parsed_ips = parse_address(address, &backup_resolver);
|
||||
if !parsed_ips.is_empty() {
|
||||
ips.extend(parsed_ips);
|
||||
} else {
|
||||
unresolved_addresses.push(address);
|
||||
}
|
||||
}
|
||||
|
||||
// If we got to this point this can only be a file path or the wrong input.
|
||||
for file_path in unresolved_addresses {
|
||||
let file_path = Path::new(file_path);
|
||||
|
||||
if !file_path.is_file() {
|
||||
warning!(
|
||||
format!("Host {file_path:?} could not be resolved."),
|
||||
input.greppable,
|
||||
input.accessible
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Ok(x) = read_ips_from_file(file_path, &backup_resolver) {
|
||||
ips.extend(x);
|
||||
} else {
|
||||
warning!(
|
||||
format!("Host {file_path:?} could not be resolved."),
|
||||
input.greppable,
|
||||
input.accessible
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ips
|
||||
}
|
||||
|
||||
/// Given a string, parse it as an host, IP address, or CIDR.
|
||||
/// This allows us to pass files as hosts or cidr or IPs easily
|
||||
/// Call this every time you have a possible IP_or_host
|
||||
fn parse_address(address: &str, resolver: &Resolver) -> Vec<IpAddr> {
|
||||
IpCidr::from_str(address)
|
||||
.map(|cidr| cidr.iter().collect())
|
||||
.ok()
|
||||
.or_else(|| {
|
||||
format!("{}:{}", &address, 80)
|
||||
.to_socket_addrs()
|
||||
.ok()
|
||||
.map(|mut iter| vec![iter.next().unwrap().ip()])
|
||||
})
|
||||
.unwrap_or_else(|| resolve_ips_from_host(address, resolver))
|
||||
}
|
||||
|
||||
/// Uses DNS to get the IPS associated with host
|
||||
fn resolve_ips_from_host(source: &str, backup_resolver: &Resolver) -> Vec<IpAddr> {
|
||||
let mut ips: Vec<std::net::IpAddr> = Vec::new();
|
||||
|
||||
if let Ok(addrs) = source.to_socket_addrs() {
|
||||
for ip in addrs {
|
||||
ips.push(ip.ip());
|
||||
}
|
||||
} else if let Ok(addrs) = backup_resolver.lookup_ip(source) {
|
||||
ips.extend(addrs.iter());
|
||||
}
|
||||
|
||||
ips
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
/// Parses an input file of IPs and uses those
|
||||
fn read_ips_from_file(
|
||||
ips: &std::path::Path,
|
||||
backup_resolver: &Resolver,
|
||||
) -> Result<Vec<std::net::IpAddr>, std::io::Error> {
|
||||
let file = File::open(ips)?;
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
let mut ips: Vec<std::net::IpAddr> = Vec::new();
|
||||
|
||||
for address_line in reader.lines() {
|
||||
if let Ok(address) = address_line {
|
||||
ips.extend(parse_address(&address, backup_resolver));
|
||||
} else {
|
||||
debug!("Line in file is not valid");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ips)
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn adjust_ulimit_size(opts: &Opts) -> usize {
|
||||
fn adjust_ulimit_size(opts: &Opts) -> u64 {
|
||||
use rlimit::Resource;
|
||||
use std::convert::TryInto;
|
||||
|
||||
if let Some(limit) = opts.ulimit {
|
||||
let limit = limit as u64;
|
||||
if Resource::NOFILE.set(limit, limit).is_ok() {
|
||||
detail!(
|
||||
format!("Automatically increasing ulimit value to {limit}."),
|
||||
@@ -255,12 +358,14 @@ fn adjust_ulimit_size(opts: &Opts) -> usize {
|
||||
}
|
||||
|
||||
let (soft, _) = Resource::NOFILE.get().unwrap();
|
||||
soft.try_into().unwrap_or(usize::MAX)
|
||||
soft
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn infer_batch_size(opts: &Opts, ulimit: usize) -> usize {
|
||||
let mut batch_size = opts.batch_size;
|
||||
fn infer_batch_size(opts: &Opts, ulimit: u64) -> u16 {
|
||||
use std::convert::TryInto;
|
||||
|
||||
let mut batch_size: u64 = opts.batch_size.into();
|
||||
|
||||
// Adjust the batch size when the ulimit value is lower than the desired batch size
|
||||
if ulimit < batch_size {
|
||||
@@ -271,7 +376,7 @@ fn infer_batch_size(opts: &Opts, ulimit: usize) -> usize {
|
||||
// When the OS supports high file limits like 8000, but the user
|
||||
// selected a batch size higher than this we should reduce it to
|
||||
// a lower number.
|
||||
if ulimit < AVERAGE_BATCH_SIZE {
|
||||
if ulimit < AVERAGE_BATCH_SIZE.into() {
|
||||
// ulimit is smaller than aveage batch size
|
||||
// user must have very small ulimit
|
||||
// decrease batch size to half of ulimit
|
||||
@@ -280,7 +385,7 @@ fn infer_batch_size(opts: &Opts, ulimit: usize) -> usize {
|
||||
batch_size = ulimit / 2;
|
||||
} else if ulimit > DEFAULT_FILE_DESCRIPTORS_LIMIT {
|
||||
info!("Batch size is now average batch size");
|
||||
batch_size = AVERAGE_BATCH_SIZE;
|
||||
batch_size = AVERAGE_BATCH_SIZE.into();
|
||||
} else {
|
||||
batch_size = ulimit - 100;
|
||||
}
|
||||
@@ -293,21 +398,23 @@ fn infer_batch_size(opts: &Opts, ulimit: usize) -> usize {
|
||||
}
|
||||
|
||||
batch_size
|
||||
.try_into()
|
||||
.expect("Couldn't fit the batch size into a u16.")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[cfg(unix)]
|
||||
use super::{adjust_ulimit_size, infer_batch_size};
|
||||
use super::{print_opening, Opts};
|
||||
use crate::{adjust_ulimit_size, infer_batch_size};
|
||||
|
||||
use crate::{parse_addresses, print_opening, Opts};
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn batch_size_lowered() {
|
||||
let opts = Opts {
|
||||
batch_size: 50_000,
|
||||
..Default::default()
|
||||
};
|
||||
let mut opts = Opts::default();
|
||||
opts.batch_size = 50_000;
|
||||
let batch_size = infer_batch_size(&opts, 120);
|
||||
|
||||
assert!(batch_size < opts.batch_size);
|
||||
@@ -316,62 +423,122 @@ mod tests {
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn batch_size_lowered_average_size() {
|
||||
let opts = Opts {
|
||||
batch_size: 50_000,
|
||||
..Default::default()
|
||||
};
|
||||
let mut opts = Opts::default();
|
||||
opts.batch_size = 50_000;
|
||||
let batch_size = infer_batch_size(&opts, 9_000);
|
||||
|
||||
assert_eq!(batch_size, 3_000);
|
||||
assert!(batch_size == 3_000);
|
||||
}
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn batch_size_equals_ulimit_lowered() {
|
||||
// because ulimit and batch size are same size, batch size is lowered
|
||||
// to ULIMIT - 100
|
||||
let opts = Opts {
|
||||
batch_size: 50_000,
|
||||
..Default::default()
|
||||
};
|
||||
let mut opts = Opts::default();
|
||||
opts.batch_size = 50_000;
|
||||
let batch_size = infer_batch_size(&opts, 5_000);
|
||||
|
||||
assert_eq!(batch_size, 4_900);
|
||||
assert!(batch_size == 4_900);
|
||||
}
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn batch_size_adjusted_2000() {
|
||||
// ulimit == batch_size
|
||||
let opts = Opts {
|
||||
batch_size: 50_000,
|
||||
ulimit: Some(2_000),
|
||||
..Default::default()
|
||||
};
|
||||
let mut opts = Opts::default();
|
||||
opts.batch_size = 50_000;
|
||||
opts.ulimit = Some(2_000);
|
||||
let batch_size = adjust_ulimit_size(&opts);
|
||||
|
||||
assert_eq!(batch_size, 2_000);
|
||||
assert!(batch_size == 2_000);
|
||||
}
|
||||
#[test]
|
||||
fn test_print_opening_no_panic() {
|
||||
let mut opts = Opts::default();
|
||||
opts.ulimit = Some(2_000);
|
||||
// print opening should not panic
|
||||
print_opening(&opts);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_high_ulimit_no_greppable_mode() {
|
||||
let opts = Opts {
|
||||
batch_size: 10,
|
||||
greppable: false,
|
||||
..Default::default()
|
||||
};
|
||||
let mut opts = Opts::default();
|
||||
opts.batch_size = 10;
|
||||
opts.greppable = false;
|
||||
|
||||
let batch_size = infer_batch_size(&opts, 1_000_000);
|
||||
|
||||
assert_eq!(batch_size, opts.batch_size);
|
||||
assert!(batch_size == opts.batch_size);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_print_opening_no_panic() {
|
||||
let opts = Opts {
|
||||
ulimit: Some(2_000),
|
||||
..Default::default()
|
||||
};
|
||||
// print opening should not panic
|
||||
print_opening(&opts);
|
||||
fn parse_correct_addresses() {
|
||||
let mut opts = Opts::default();
|
||||
opts.addresses = vec!["127.0.0.1".to_owned(), "192.168.0.0/30".to_owned()];
|
||||
let ips = parse_addresses(&opts);
|
||||
|
||||
assert_eq!(
|
||||
ips,
|
||||
[
|
||||
Ipv4Addr::new(127, 0, 0, 1),
|
||||
Ipv4Addr::new(192, 168, 0, 0),
|
||||
Ipv4Addr::new(192, 168, 0, 1),
|
||||
Ipv4Addr::new(192, 168, 0, 2),
|
||||
Ipv4Addr::new(192, 168, 0, 3)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_correct_host_addresses() {
|
||||
let mut opts = Opts::default();
|
||||
opts.addresses = vec!["google.com".to_owned()];
|
||||
let ips = parse_addresses(&opts);
|
||||
|
||||
assert_eq!(ips.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_correct_and_incorrect_addresses() {
|
||||
let mut opts = Opts::default();
|
||||
opts.addresses = vec!["127.0.0.1".to_owned(), "im_wrong".to_owned()];
|
||||
let ips = parse_addresses(&opts);
|
||||
|
||||
assert_eq!(ips, [Ipv4Addr::new(127, 0, 0, 1),]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_incorrect_addresses() {
|
||||
let mut opts = Opts::default();
|
||||
opts.addresses = vec!["im_wrong".to_owned(), "300.10.1.1".to_owned()];
|
||||
let ips = parse_addresses(&opts);
|
||||
|
||||
assert!(ips.is_empty());
|
||||
}
|
||||
#[test]
|
||||
fn parse_hosts_file_and_incorrect_hosts() {
|
||||
// Host file contains IP, Hosts, incorrect IPs, incorrect hosts
|
||||
let mut opts = Opts::default();
|
||||
opts.addresses = vec!["fixtures/hosts.txt".to_owned()];
|
||||
let ips = parse_addresses(&opts);
|
||||
assert_eq!(ips.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_empty_hosts_file() {
|
||||
// Host file contains IP, Hosts, incorrect IPs, incorrect hosts
|
||||
let mut opts = Opts::default();
|
||||
opts.addresses = vec!["fixtures/empty_hosts.txt".to_owned()];
|
||||
let ips = parse_addresses(&opts);
|
||||
assert_eq!(ips.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_naughty_host_file() {
|
||||
// Host file contains IP, Hosts, incorrect IPs, incorrect hosts
|
||||
let mut opts = Opts::default();
|
||||
opts.addresses = vec!["fixtures/naughty_string.txt".to_owned()];
|
||||
let ips = parse_addresses(&opts);
|
||||
assert_eq!(ips.len(), 0);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
//! Provides a means to hold configuration options specifically for port scanning.
|
||||
mod range_iterator;
|
||||
use crate::input::{PortRange, ScanOrder};
|
||||
use rand::rng;
|
||||
use super::{PortRange, ScanOrder};
|
||||
use rand::seq::SliceRandom;
|
||||
use rand::thread_rng;
|
||||
use range_iterator::RangeIterator;
|
||||
|
||||
/// Represents options of port scanning.
|
||||
@@ -35,7 +34,7 @@ impl PortStrategy {
|
||||
}
|
||||
ScanOrder::Serial => PortStrategy::Manual(ports.unwrap()),
|
||||
ScanOrder::Random => {
|
||||
let mut rng = rng();
|
||||
let mut rng = thread_rng();
|
||||
let mut ports = ports.unwrap();
|
||||
ports.shuffle(&mut rng);
|
||||
PortStrategy::Manual(ports)
|
||||
@@ -68,7 +67,7 @@ pub struct SerialRange {
|
||||
|
||||
impl RangeOrder for SerialRange {
|
||||
fn generate(&self) -> Vec<u16> {
|
||||
(self.start..=self.end).collect()
|
||||
(self.start..self.end).collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,14 +97,14 @@ impl RangeOrder for RandomRange {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::PortStrategy;
|
||||
use crate::input::{PortRange, ScanOrder};
|
||||
use crate::{PortRange, ScanOrder};
|
||||
|
||||
#[test]
|
||||
fn serial_strategy_with_range() {
|
||||
let range = PortRange { start: 1, end: 100 };
|
||||
let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Serial);
|
||||
let result = strategy.order();
|
||||
let expected_range = (1..=100).collect::<Vec<u16>>();
|
||||
let expected_range = (1..100).into_iter().collect::<Vec<u16>>();
|
||||
assert_eq!(expected_range, result);
|
||||
}
|
||||
#[test]
|
||||
@@ -113,7 +112,7 @@ mod tests {
|
||||
let range = PortRange { start: 1, end: 100 };
|
||||
let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random);
|
||||
let mut result = strategy.order();
|
||||
let expected_range = (1..=100).collect::<Vec<u16>>();
|
||||
let expected_range = (1..100).into_iter().collect::<Vec<u16>>();
|
||||
assert_ne!(expected_range, result);
|
||||
|
||||
result.sort_unstable();
|
||||
@@ -131,7 +130,7 @@ mod tests {
|
||||
fn random_strategy_with_ports() {
|
||||
let strategy = PortStrategy::pick(&None, Some((1..10).collect()), ScanOrder::Random);
|
||||
let mut result = strategy.order();
|
||||
let expected_range = (1..10).collect::<Vec<u16>>();
|
||||
let expected_range = (1..10).into_iter().collect::<Vec<u16>>();
|
||||
assert_ne!(expected_range, result);
|
||||
|
||||
result.sort_unstable();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use gcd::Gcd;
|
||||
use rand::RngExt;
|
||||
use rand::Rng;
|
||||
use std::convert::TryInto;
|
||||
|
||||
pub struct RangeIterator {
|
||||
@@ -15,20 +15,20 @@ pub struct RangeIterator {
|
||||
///
|
||||
/// For more information: <https://en.wikipedia.org/wiki/Linear_congruential_generator>
|
||||
impl RangeIterator {
|
||||
/// Receives the start and end of a range and normalize
|
||||
/// Receives the the start and end of a range and normalize
|
||||
/// these values before selecting a coprime for the end of the range
|
||||
/// which will server as the step for the algorithm.
|
||||
///
|
||||
/// For example, the range `1000-2500` will be normalized to `0-1500`
|
||||
/// before going through the algorithm.
|
||||
pub fn new(start: u32, end: u32) -> Self {
|
||||
let normalized_end = end - start + 1;
|
||||
let normalized_end = end - start;
|
||||
let step = pick_random_coprime(normalized_end);
|
||||
|
||||
// Randomly choose a number within the range to be the first
|
||||
// and assign it as a pick.
|
||||
let mut rng = rand::rng();
|
||||
let normalized_first_pick = rng.random_range(0..normalized_end);
|
||||
let mut rng = rand::thread_rng();
|
||||
let normalized_first_pick = rng.gen_range(0, normalized_end);
|
||||
|
||||
Self {
|
||||
active: true,
|
||||
@@ -83,14 +83,14 @@ fn pick_random_coprime(end: u32) -> u32 {
|
||||
let range_boundary = end / 4;
|
||||
let lower_range = range_boundary;
|
||||
let upper_range = end - range_boundary;
|
||||
let mut rng = rand::rng();
|
||||
let mut candidate = rng.random_range(lower_range..upper_range);
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut candidate = rng.gen_range(lower_range, upper_range);
|
||||
|
||||
for _ in 0..10 {
|
||||
if end.gcd(candidate) == 1 {
|
||||
return candidate;
|
||||
}
|
||||
candidate = rng.random_range(lower_range..upper_range);
|
||||
candidate = rng.gen_range(lower_range, upper_range);
|
||||
}
|
||||
|
||||
end - 1
|
||||
@@ -103,23 +103,23 @@ mod tests {
|
||||
#[test]
|
||||
fn range_iterator_iterates_through_the_entire_range() {
|
||||
let result = generate_sorted_range(1, 10);
|
||||
let expected_range = (1..=10).collect::<Vec<u16>>();
|
||||
let expected_range = (1..10).into_iter().collect::<Vec<u16>>();
|
||||
assert_eq!(expected_range, result);
|
||||
|
||||
let result = generate_sorted_range(1, 100);
|
||||
let expected_range = (1..=100).collect::<Vec<u16>>();
|
||||
let expected_range = (1..100).into_iter().collect::<Vec<u16>>();
|
||||
assert_eq!(expected_range, result);
|
||||
|
||||
let result = generate_sorted_range(1, 1000);
|
||||
let expected_range = (1..=1000).collect::<Vec<u16>>();
|
||||
let expected_range = (1..1000).into_iter().collect::<Vec<u16>>();
|
||||
assert_eq!(expected_range, result);
|
||||
|
||||
let result = generate_sorted_range(1, 65_535);
|
||||
let expected_range = (1..=65_535).collect::<Vec<u16>>();
|
||||
let expected_range = (1..65_535).into_iter().collect::<Vec<u16>>();
|
||||
assert_eq!(expected_range, result);
|
||||
|
||||
let result = generate_sorted_range(1000, 2000);
|
||||
let expected_range = (1000..=2000).collect::<Vec<u16>>();
|
||||
let expected_range = (1000..2000).into_iter().collect::<Vec<u16>>();
|
||||
assert_eq!(expected_range, result);
|
||||
}
|
||||
|
||||
|
||||
+150
-166
@@ -1,17 +1,13 @@
|
||||
//! Core functionality for actual scanning behaviour.
|
||||
use crate::generated::get_parsed_data;
|
||||
use crate::port_strategy::PortStrategy;
|
||||
use log::debug;
|
||||
use super::PortStrategy;
|
||||
|
||||
mod socket_iterator;
|
||||
use socket_iterator::SocketIterator;
|
||||
|
||||
use async_std::io;
|
||||
use async_std::net::TcpStream;
|
||||
use async_std::prelude::*;
|
||||
use async_std::{io, net::UdpSocket};
|
||||
use colored::Colorize;
|
||||
use futures::stream::FuturesUnordered;
|
||||
use std::collections::BTreeMap;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
net::{IpAddr, Shutdown, SocketAddr},
|
||||
@@ -29,29 +25,23 @@ use std::{
|
||||
#[derive(Debug)]
|
||||
pub struct Scanner {
|
||||
ips: Vec<IpAddr>,
|
||||
batch_size: usize,
|
||||
batch_size: u16,
|
||||
timeout: Duration,
|
||||
tries: NonZeroU8,
|
||||
greppable: bool,
|
||||
port_strategy: PortStrategy,
|
||||
accessible: bool,
|
||||
exclude_ports: Vec<u16>,
|
||||
udp: bool,
|
||||
}
|
||||
|
||||
// Allowing too many arguments for clippy.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
impl Scanner {
|
||||
pub fn new(
|
||||
ips: &[IpAddr],
|
||||
batch_size: usize,
|
||||
batch_size: u16,
|
||||
timeout: Duration,
|
||||
tries: u8,
|
||||
greppable: bool,
|
||||
port_strategy: PortStrategy,
|
||||
accessible: bool,
|
||||
exclude_ports: Vec<u16>,
|
||||
udp: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
batch_size,
|
||||
@@ -61,45 +51,36 @@ impl Scanner {
|
||||
port_strategy,
|
||||
ips: ips.iter().map(ToOwned::to_owned).collect(),
|
||||
accessible,
|
||||
exclude_ports,
|
||||
udp,
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs scan_range with chunk sizes
|
||||
/// If you want to run RustScan normally, this is the entry point used
|
||||
/// Returns all open ports as `Vec<u16>`
|
||||
/// Returns all open ports as Vec<u16>
|
||||
pub async fn run(&self) -> Vec<SocketAddr> {
|
||||
let ports: Vec<u16> = self
|
||||
.port_strategy
|
||||
.order()
|
||||
.iter()
|
||||
.filter(|&port| !self.exclude_ports.contains(port))
|
||||
.copied()
|
||||
.collect();
|
||||
let ports: Vec<u16> = self.port_strategy.order();
|
||||
let mut socket_iterator: SocketIterator = SocketIterator::new(&self.ips, &ports);
|
||||
let mut open_sockets: Vec<SocketAddr> = Vec::new();
|
||||
let mut ftrs = FuturesUnordered::new();
|
||||
let mut errors: HashSet<String> = HashSet::new();
|
||||
let udp_map = get_parsed_data();
|
||||
let mut errors: HashSet<String> = HashSet::with_capacity(self.ips.len() * 1000);
|
||||
|
||||
for _ in 0..self.batch_size {
|
||||
if let Some(socket) = socket_iterator.next() {
|
||||
ftrs.push(self.scan_socket(socket, udp_map.clone()));
|
||||
ftrs.push(self.scan_socket(socket));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Start scanning sockets. \nBatch size {}\nNumber of ip-s {}\nNumber of ports {}\nTargets all together {} ",
|
||||
debug!("Start scanning sockets. \nBatch size {}\nNumber of ip-s {}\nNumber of ports {}\nTargets all together {} ",
|
||||
self.batch_size,
|
||||
self.ips.len(),
|
||||
ports.len(),
|
||||
&ports.len(),
|
||||
(self.ips.len() * ports.len()));
|
||||
|
||||
while let Some(result) = ftrs.next().await {
|
||||
if let Some(socket) = socket_iterator.next() {
|
||||
ftrs.push(self.scan_socket(socket, udp_map.clone()));
|
||||
ftrs.push(self.scan_socket(socket));
|
||||
}
|
||||
|
||||
match result {
|
||||
@@ -112,45 +93,45 @@ impl Scanner {
|
||||
}
|
||||
}
|
||||
}
|
||||
debug!("Typical socket connection errors {errors:?}");
|
||||
debug!("Open Sockets found: {:?}", open_sockets);
|
||||
debug!("Typical socket connection errors {:?}", errors);
|
||||
debug!("Open Sockets found: {:?}", &open_sockets);
|
||||
open_sockets
|
||||
}
|
||||
|
||||
/// Given a socket, scan it self.tries times.
|
||||
/// Turns the address into a SocketAddr
|
||||
/// Deals with the `<result>` type
|
||||
/// Deals with the <result> type
|
||||
/// If it experiences error ErrorKind::Other then too many files are open and it Panics!
|
||||
/// Else any other error, it returns the error in Result as a string
|
||||
/// If no errors occur, it returns the port number in Result to signify the port is open.
|
||||
/// This function mainly deals with the logic of Results handling.
|
||||
/// # Example
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// scanner.scan_socket(socket)
|
||||
/// ```
|
||||
/// self.scan_socket(socket)
|
||||
///
|
||||
/// Note: `self` must contain `self.ip`.
|
||||
async fn scan_socket(
|
||||
&self,
|
||||
socket: SocketAddr,
|
||||
udp_map: BTreeMap<Vec<u16>, Vec<u8>>,
|
||||
) -> io::Result<SocketAddr> {
|
||||
if self.udp {
|
||||
return self.scan_udp_socket(socket, udp_map).await;
|
||||
}
|
||||
|
||||
async fn scan_socket(&self, socket: SocketAddr) -> io::Result<SocketAddr> {
|
||||
let tries = self.tries.get();
|
||||
|
||||
for nr_try in 1..=tries {
|
||||
match self.connect(socket).await {
|
||||
Ok(tcp_stream) => {
|
||||
debug!("Connection was successful, shutting down stream {}", socket);
|
||||
if let Err(e) = tcp_stream.shutdown(Shutdown::Both) {
|
||||
debug!("Shutdown stream error {}", e);
|
||||
Ok(x) => {
|
||||
debug!(
|
||||
"Connection was successful, shutting down stream {}",
|
||||
&socket
|
||||
);
|
||||
if let Err(e) = x.shutdown(Shutdown::Both) {
|
||||
debug!("Shutdown stream error {}", &e);
|
||||
}
|
||||
if !self.greppable {
|
||||
if self.accessible {
|
||||
println!("Open {socket}");
|
||||
} else {
|
||||
println!("Open {}", socket.to_string().purple());
|
||||
}
|
||||
}
|
||||
self.fmt_ports(socket);
|
||||
|
||||
debug!("Return Ok after {nr_try} tries");
|
||||
debug!("Return Ok after {} tries", nr_try);
|
||||
return Ok(socket);
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -161,7 +142,7 @@ impl Scanner {
|
||||
if nr_try == tries {
|
||||
error_string.push(' ');
|
||||
error_string.push_str(&socket.ip().to_string());
|
||||
return Err(io::Error::other(error_string));
|
||||
return Err(io::Error::new(io::ErrorKind::Other, error_string));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -169,45 +150,16 @@ impl Scanner {
|
||||
unreachable!();
|
||||
}
|
||||
|
||||
async fn scan_udp_socket(
|
||||
&self,
|
||||
socket: SocketAddr,
|
||||
udp_map: BTreeMap<Vec<u16>, Vec<u8>>,
|
||||
) -> io::Result<SocketAddr> {
|
||||
let mut payload: Vec<u8> = Vec::new();
|
||||
for (key, value) in udp_map {
|
||||
if key.contains(&socket.port()) {
|
||||
payload = value;
|
||||
}
|
||||
}
|
||||
|
||||
let tries = self.tries.get();
|
||||
for _ in 1..=tries {
|
||||
match self.udp_scan(socket, &payload, self.timeout).await {
|
||||
Ok(true) => return Ok(socket),
|
||||
Ok(false) => continue,
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
Err(io::Error::other(format!(
|
||||
"UDP scan timed-out for all tries on socket {socket}"
|
||||
)))
|
||||
}
|
||||
|
||||
/// Performs the connection to the socket with timeout
|
||||
/// # Example
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// # use std::net::{IpAddr, Ipv6Addr, SocketAddr};
|
||||
/// let port: u16 = 80;
|
||||
/// // ip is an IpAddr type
|
||||
/// let ip = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1));
|
||||
/// let socket = SocketAddr::new(ip, port);
|
||||
/// scanner.connect(socket);
|
||||
/// // returns Result which is either Ok(stream) for port is open, or Er for port is closed.
|
||||
/// // Timeout occurs after self.timeout seconds
|
||||
/// ```
|
||||
/// let port: u16 = 80
|
||||
/// // ip is an IpAddr type
|
||||
/// let ip = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1));
|
||||
/// let socket = SocketAddr::new(ip, port);
|
||||
/// self.connect(socket)
|
||||
/// // returns Result which is either Ok(stream) for port is open, or Er for port is closed.
|
||||
/// // Timeout occurs after self.timeout seconds
|
||||
///
|
||||
async fn connect(&self, socket: SocketAddr) -> io::Result<TcpStream> {
|
||||
let stream = io::timeout(
|
||||
@@ -217,88 +169,120 @@ impl Scanner {
|
||||
.await?;
|
||||
Ok(stream)
|
||||
}
|
||||
}
|
||||
|
||||
/// Binds to a UDP socket so we can send and receive packets
|
||||
/// # Example
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// # use std::net::{IpAddr, Ipv6Addr, SocketAddr};
|
||||
/// let port: u16 = 80;
|
||||
/// // ip is an IpAddr type
|
||||
/// let ip = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1));
|
||||
/// let socket = SocketAddr::new(ip, port);
|
||||
/// scanner.udp_bind(socket);
|
||||
/// // returns Result which is either Ok(stream) for port is open, or Err for port is closed.
|
||||
/// // Timeout occurs after self.timeout seconds
|
||||
/// ```
|
||||
///
|
||||
async fn udp_bind(&self, socket: SocketAddr) -> io::Result<UdpSocket> {
|
||||
let local_addr = match socket {
|
||||
SocketAddr::V4(_) => "0.0.0.0:0".parse::<SocketAddr>().unwrap(),
|
||||
SocketAddr::V6(_) => "[::]:0".parse::<SocketAddr>().unwrap(),
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{PortRange, ScanOrder};
|
||||
use async_std::task::block_on;
|
||||
use std::{net::IpAddr, time::Duration};
|
||||
|
||||
#[test]
|
||||
fn scanner_runs() {
|
||||
// Makes sure the program still runs and doesn't panic
|
||||
let addrs = vec!["127.0.0.1".parse::<IpAddr>().unwrap()];
|
||||
let range = PortRange {
|
||||
start: 1,
|
||||
end: 1_000,
|
||||
};
|
||||
|
||||
UdpSocket::bind(local_addr).await
|
||||
let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random);
|
||||
let scanner = Scanner::new(
|
||||
&addrs,
|
||||
10,
|
||||
Duration::from_millis(100),
|
||||
1,
|
||||
true,
|
||||
strategy,
|
||||
true,
|
||||
);
|
||||
block_on(scanner.run());
|
||||
// if the scan fails, it wouldn't be able to assert_eq! as it panicked!
|
||||
assert_eq!(1, 1);
|
||||
}
|
||||
|
||||
/// Performs a UDP scan on the specified socket with a payload and wait duration
|
||||
/// # Example
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// # use std::net::{IpAddr, Ipv6Addr, SocketAddr};
|
||||
/// # use std::time::Duration;
|
||||
/// let port: u16 = 123;
|
||||
/// // ip is an IpAddr type
|
||||
/// let ip = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1));
|
||||
/// let socket = SocketAddr::new(ip, port);
|
||||
/// let payload = vec![0, 1, 2, 3];
|
||||
/// let wait = Duration::from_secs(1);
|
||||
/// let result = scanner.udp_scan(socket, payload, wait).await;
|
||||
/// // returns Result which is either Ok(true) if response received, or Ok(false) if timed out.
|
||||
/// // Err is returned for other I/O errors.
|
||||
async fn udp_scan(
|
||||
&self,
|
||||
socket: SocketAddr,
|
||||
payload: &[u8],
|
||||
wait: Duration,
|
||||
) -> io::Result<bool> {
|
||||
match self.udp_bind(socket).await {
|
||||
Ok(udp_socket) => {
|
||||
let mut buf = [0u8; 1024];
|
||||
|
||||
udp_socket.connect(socket).await?;
|
||||
udp_socket.send(payload).await?;
|
||||
|
||||
match io::timeout(wait, udp_socket.recv(&mut buf)).await {
|
||||
Ok(size) => {
|
||||
debug!("Received {size} bytes");
|
||||
self.fmt_ports(socket);
|
||||
Ok(true)
|
||||
}
|
||||
Err(e) => {
|
||||
if e.kind() == io::ErrorKind::TimedOut {
|
||||
Ok(false)
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Err E binding sock {e:?}");
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn ipv6_scanner_runs() {
|
||||
// Makes sure the program still runs and doesn't panic
|
||||
let addrs = vec!["::1".parse::<IpAddr>().unwrap()];
|
||||
let range = PortRange {
|
||||
start: 1,
|
||||
end: 1_000,
|
||||
};
|
||||
let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random);
|
||||
let scanner = Scanner::new(
|
||||
&addrs,
|
||||
10,
|
||||
Duration::from_millis(100),
|
||||
1,
|
||||
true,
|
||||
strategy,
|
||||
true,
|
||||
);
|
||||
block_on(scanner.run());
|
||||
// if the scan fails, it wouldn't be able to assert_eq! as it panicked!
|
||||
assert_eq!(1, 1);
|
||||
}
|
||||
#[test]
|
||||
fn quad_zero_scanner_runs() {
|
||||
let addrs = vec!["0.0.0.0".parse::<IpAddr>().unwrap()];
|
||||
let range = PortRange {
|
||||
start: 1,
|
||||
end: 1_000,
|
||||
};
|
||||
let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random);
|
||||
let scanner = Scanner::new(
|
||||
&addrs,
|
||||
10,
|
||||
Duration::from_millis(100),
|
||||
1,
|
||||
true,
|
||||
strategy,
|
||||
true,
|
||||
);
|
||||
block_on(scanner.run());
|
||||
assert_eq!(1, 1);
|
||||
}
|
||||
#[test]
|
||||
fn google_dns_runs() {
|
||||
let addrs = vec!["8.8.8.8".parse::<IpAddr>().unwrap()];
|
||||
let range = PortRange {
|
||||
start: 400,
|
||||
end: 445,
|
||||
};
|
||||
let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random);
|
||||
let scanner = Scanner::new(
|
||||
&addrs,
|
||||
10,
|
||||
Duration::from_millis(100),
|
||||
1,
|
||||
true,
|
||||
strategy,
|
||||
true,
|
||||
);
|
||||
block_on(scanner.run());
|
||||
assert_eq!(1, 1);
|
||||
}
|
||||
#[test]
|
||||
fn infer_ulimit_lowering_no_panic() {
|
||||
// Test behaviour on MacOS where ulimit is not automatically lowered
|
||||
let addrs = vec!["8.8.8.8".parse::<IpAddr>().unwrap()];
|
||||
|
||||
/// Formats and prints the port status
|
||||
fn fmt_ports(&self, socket: SocketAddr) {
|
||||
if !self.greppable {
|
||||
if self.accessible {
|
||||
println!("Open {socket}");
|
||||
} else {
|
||||
println!("Open {}", socket.to_string().purple());
|
||||
}
|
||||
}
|
||||
// mac should have this automatically scaled down
|
||||
let range = PortRange {
|
||||
start: 400,
|
||||
end: 600,
|
||||
};
|
||||
let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random);
|
||||
let scanner = Scanner::new(
|
||||
&addrs,
|
||||
10,
|
||||
Duration::from_millis(100),
|
||||
1,
|
||||
true,
|
||||
strategy,
|
||||
true,
|
||||
);
|
||||
block_on(scanner.run());
|
||||
assert_eq!(1, 1);
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ impl<'s> SocketIterator<'s> {
|
||||
}
|
||||
|
||||
#[allow(clippy::doc_link_with_quotes)]
|
||||
impl Iterator for SocketIterator<'_> {
|
||||
impl<'s> Iterator for SocketIterator<'s> {
|
||||
type Item = SocketAddr;
|
||||
|
||||
/// Returns a socket based on the combination of one of the provided
|
||||
|
||||
+151
-187
@@ -1,158 +1,130 @@
|
||||
//! Scripting Engine to run scripts based on tags.
|
||||
//!
|
||||
//! Scripting engine to run scripts based on tags.
|
||||
//! This module serves to filter and run the scripts selected by the user.
|
||||
//!
|
||||
//! A new commandline and configuration file option was added.
|
||||
//!
|
||||
//! ## `--scripts`
|
||||
//! --scripts
|
||||
//!
|
||||
//! ### `default`
|
||||
//! default
|
||||
//! This is the default behavior, like as it was from the beginning of RustScan.
|
||||
//! The user do not have to chose anything for this. This is the only script embedded in RustScan running as default.
|
||||
//!
|
||||
//! This is the default behavior, like as it was from the beginning of RustScan.
|
||||
//! none
|
||||
//! The user have to use the --scripts none commandline argument or scripts = "none" in the config file.
|
||||
//! None of the scripts will run, this replaces the removed --no-nmap option.
|
||||
//!
|
||||
//! The user do not have to chose anything for this. This is the only script
|
||||
//! embedded in RustScan running as default.
|
||||
//!
|
||||
//! ### `none`
|
||||
//!
|
||||
//! The user have to use the `--scripts none` commandline argument or `scripts =
|
||||
//! "none"` in the config file.
|
||||
//!
|
||||
//! None of the scripts will run, this replaces the removed `--no-nmap` option.
|
||||
//!
|
||||
//! ### `custom`
|
||||
//!
|
||||
//! The user have to use the `--scripts custom` commandline argument or
|
||||
//! `scripts = "custom"` in the config file.
|
||||
//!
|
||||
//! RustScan will look for the script configuration file in the user's home
|
||||
//! dir: `home_dir/.rustscan_scripts.toml`
|
||||
//!
|
||||
//! The config file have 3 optional fields: `tag`, `developer` and `port`. Just
|
||||
//! the `tag` field will be used forther in the process.
|
||||
//!
|
||||
//! RustScan will also look for available scripts in the user's home dir:
|
||||
//! `home_dir/.rustscan_scripts` and will try to read all the files, and parse
|
||||
//! them into a vector of [`ScriptFile`].
|
||||
//!
|
||||
//! Filtering on tags means the tags found in the `rustscan_scripts.toml` file
|
||||
//! will also have to be present in the [`ScriptFile`], otherwise the script
|
||||
//! will not be selected.
|
||||
//!
|
||||
//! All of the `rustscan_script.toml` tags have to be present at minimum in a
|
||||
//! [`ScriptFile`] to get selected, but can be also more.
|
||||
//! custom
|
||||
//! The user have to use the --scripts custom commandline argument or scripts = "custom" in the config file.
|
||||
//! Rustscan will look for the script configuration file in the user's home dir: home_dir/.rustscan_scripts.toml
|
||||
//! The config file have 3 optional fields, tag, developer and port. Just the tag field will be used forther in the process.
|
||||
//! RustScan will also look for available scripts in the user's home dir: home_dir/.rustscan_scripts
|
||||
//! and will try to read all the files, and parse them into a vector of ScriptFiles.
|
||||
//! Filtering on tags means the tags found in the rustscan_scripts.toml file will also have to be present in the Scriptfile,
|
||||
//! otherwise the script will not be selected.
|
||||
//! All of the rustscan_script.toml tags have to be present at minimum in a Scriptfile to get selected, but can be also more.
|
||||
//!
|
||||
//! Config file example:
|
||||
//!
|
||||
//! - `fixtures/test_rustscan_scripts.toml`
|
||||
//! fixtures/test_rustscan_scripts.toml
|
||||
//!
|
||||
//! Script file examples:
|
||||
//! fixtures/test_script.py
|
||||
//! fixtures/test_script.pl
|
||||
//! fixtures/test_script.sh
|
||||
//! fixtures/test_script.txt
|
||||
//!
|
||||
//! - `fixtures/test_script.py`
|
||||
//! - `fixtures/test_script.pl`
|
||||
//! - `fixtures/test_script.sh`
|
||||
//! - `fixtures/test_script.txt`
|
||||
//! call_format in script files can be of 2 variants.
|
||||
//! One is where all of the possible tags {{script}} {{ip}} {{port}} are there.
|
||||
//! The {{script}} part will be replaced with the scriptfile full path gathered while parsing available scripts.
|
||||
//! The {{ip}} part will be replaced with the ip we got from the scan.
|
||||
//! The {{port}} part will be reaplced with the ports separated with the ports_separator found in the script file
|
||||
//!
|
||||
//! `call_format` in script files can be of 2 variants:
|
||||
//! And when there is only {{ip}} and {{port}} is in the format, only those will be replaced with the arguments from the scan.
|
||||
//! This makes it easy to run a system installed command like nmap, and give any kind of arguments to it.
|
||||
//!
|
||||
//! One is where all of the possible tags `{{script}}`, `{{ip}}` and `{{port}}`
|
||||
//! are there.
|
||||
//!
|
||||
//! - The `{{script}}` part will be replaced with the scriptfile full path
|
||||
//! gathered while parsing available scripts.
|
||||
//! - The `{{ip}}` part will be replaced with the ip we got from the scan.
|
||||
//! - The `{{port}}` part will be reaplced with the ports separated with the
|
||||
//! `ports_separator` found in the script file
|
||||
//!
|
||||
//! And when there is only `{{ip}}` and `{{port}}` is in the format, only those
|
||||
//! will be replaced with the arguments from the scan.
|
||||
//!
|
||||
//! This makes it easy to run a system installed command like `nmap`, and give
|
||||
//! any kind of arguments to it.
|
||||
//!
|
||||
//! If the format is different, the script will be silently discarded and will
|
||||
//! not run. With the `Debug` option it's possible to see where it goes wrong.
|
||||
//! If the format is different, the script will be silently discarded and will not run. With the Debug option it's possible to see where it goes wrong.
|
||||
|
||||
#![allow(clippy::module_name_repetitions)]
|
||||
|
||||
use crate::input::ScriptsRequired;
|
||||
use anyhow::{anyhow, Result};
|
||||
use log::debug;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
use std::convert::TryInto;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{self, prelude::*};
|
||||
use std::net::IpAddr;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::string::ToString;
|
||||
use subprocess::{Exec, ExitStatus};
|
||||
use text_placeholder::Template;
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
|
||||
static DEFAULT: &str = r#"tags = ["core_approved", "RustScan", "default"]
|
||||
developer = [ "RustScan", "https://github.com/RustScan" ]
|
||||
ports_separator = ","
|
||||
call_format = "nmap -vvv -p {{port}} -{{ipversion}} {{ip}}"
|
||||
call_format = "nmap -vvv -p {{port}} {{ip}}"
|
||||
"#;
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
pub fn init_scripts(scripts: &ScriptsRequired) -> Result<Vec<ScriptFile>> {
|
||||
pub fn init_scripts(scripts: ScriptsRequired) -> Result<Vec<ScriptFile>> {
|
||||
let mut scripts_to_run: Vec<ScriptFile> = Vec::new();
|
||||
|
||||
match scripts {
|
||||
ScriptsRequired::None => {}
|
||||
ScriptsRequired::None => Ok(scripts_to_run),
|
||||
ScriptsRequired::Default => {
|
||||
let default_script =
|
||||
toml::from_str::<ScriptFile>(DEFAULT).expect("Failed to parse Script file.");
|
||||
scripts_to_run.push(default_script);
|
||||
Ok(scripts_to_run)
|
||||
}
|
||||
ScriptsRequired::Custom => {
|
||||
let script_config = ScriptConfig::read_config()?;
|
||||
debug!("Script config \n{script_config:?}");
|
||||
|
||||
let script_dir_base = if let Some(config_directory) = &script_config.directory {
|
||||
PathBuf::from(config_directory)
|
||||
} else {
|
||||
dirs::home_dir().ok_or_else(|| anyhow!("Could not infer scripts path."))?
|
||||
let Some(scripts_dir_base) = dirs::home_dir() else {
|
||||
return Err(anyhow!("Could not infer scripts path."));
|
||||
};
|
||||
|
||||
let script_paths = find_scripts(script_dir_base)?;
|
||||
debug!("Scripts paths \n{script_paths:?}");
|
||||
let script_paths = match find_scripts(scripts_dir_base) {
|
||||
Ok(script_paths) => script_paths,
|
||||
Err(e) => return Err(anyhow!(e)),
|
||||
};
|
||||
debug!("Scripts paths \n{:?}", script_paths);
|
||||
|
||||
let parsed_scripts = parse_scripts(script_paths);
|
||||
debug!("Scripts parsed \n{parsed_scripts:?}");
|
||||
debug!("Scripts parsed \n{:?}", parsed_scripts);
|
||||
|
||||
let script_config = match ScriptConfig::read_config() {
|
||||
Ok(script_config) => script_config,
|
||||
Err(e) => return Err(anyhow!(e)),
|
||||
};
|
||||
debug!("Script config \n{:?}", script_config);
|
||||
|
||||
// Only Scripts that contain all the tags found in ScriptConfig will be selected.
|
||||
if let Some(config_hashset) = script_config.tags {
|
||||
for script in parsed_scripts {
|
||||
if let Some(script_hashset) = &script.tags {
|
||||
if script_hashset
|
||||
.iter()
|
||||
.all(|tag| config_hashset.contains(tag))
|
||||
{
|
||||
scripts_to_run.push(script);
|
||||
if script_config.tags.is_some() {
|
||||
let config_hashset: HashSet<String> =
|
||||
script_config.tags.unwrap().into_iter().collect();
|
||||
for script in &parsed_scripts {
|
||||
if script.tags.is_some() {
|
||||
let script_hashset: HashSet<String> =
|
||||
script.tags.clone().unwrap().into_iter().collect();
|
||||
if config_hashset.is_subset(&script_hashset) {
|
||||
scripts_to_run.push(script.clone());
|
||||
} else {
|
||||
debug!(
|
||||
"\nScript tags does not match config tags {:?} {}",
|
||||
script_hashset,
|
||||
script.path.unwrap().display()
|
||||
&script_hashset,
|
||||
script.path.clone().unwrap().display()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
debug!("\nScript(s) to run {scripts_to_run:?}");
|
||||
debug!("\nScript(s) to run {:?}", scripts_to_run);
|
||||
Ok(scripts_to_run)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(scripts_to_run)
|
||||
}
|
||||
|
||||
pub fn parse_scripts(scripts: Vec<PathBuf>) -> Vec<ScriptFile> {
|
||||
let mut parsed_scripts: Vec<ScriptFile> = Vec::with_capacity(scripts.len());
|
||||
for script in scripts {
|
||||
debug!("Parsing script {}", script.display());
|
||||
debug!("Parsing script {}", &script.display());
|
||||
if let Some(script_file) = ScriptFile::new(script) {
|
||||
parsed_scripts.push(script_file);
|
||||
}
|
||||
@@ -190,14 +162,12 @@ struct ExecPartsScript {
|
||||
script: String,
|
||||
ip: String,
|
||||
port: String,
|
||||
ipversion: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ExecParts {
|
||||
ip: String,
|
||||
port: String,
|
||||
ipversion: String,
|
||||
}
|
||||
|
||||
impl Script {
|
||||
@@ -224,7 +194,7 @@ impl Script {
|
||||
// Some variables get changed before read, and compiler throws warning on warn(unused_assignments)
|
||||
#[allow(unused_assignments)]
|
||||
pub fn run(self) -> Result<String> {
|
||||
debug!("run self {:?}", self);
|
||||
debug!("run self {:?}", &self);
|
||||
|
||||
let separator = self.ports_separator.unwrap_or_else(|| ",".into());
|
||||
|
||||
@@ -252,77 +222,51 @@ impl Script {
|
||||
script: self.path.unwrap().to_str().unwrap().to_string(),
|
||||
ip: self.ip.to_string(),
|
||||
port: ports_str,
|
||||
ipversion: match &self.ip {
|
||||
IpAddr::V4(_) => String::from("4"),
|
||||
IpAddr::V6(_) => String::from("6"),
|
||||
},
|
||||
};
|
||||
to_run = default_template.fill_with_struct(&exec_parts_script)?;
|
||||
} else {
|
||||
let exec_parts: ExecParts = ExecParts {
|
||||
ip: self.ip.to_string(),
|
||||
port: ports_str,
|
||||
ipversion: match &self.ip {
|
||||
IpAddr::V4(_) => String::from("4"),
|
||||
IpAddr::V6(_) => String::from("6"),
|
||||
},
|
||||
};
|
||||
to_run = default_template.fill_with_struct(&exec_parts)?;
|
||||
}
|
||||
debug!("\nScript format to run {to_run}");
|
||||
execute_script(&to_run)
|
||||
debug!("\nScript format to run {}", to_run);
|
||||
|
||||
let arguments = shell_words::split(&to_run).expect("Failed to parse script arguments");
|
||||
|
||||
execute_script(arguments)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
fn execute_script(script: &str) -> Result<String> {
|
||||
debug!("\nScript arguments {script}");
|
||||
|
||||
let (cmd, arg) = if cfg!(unix) {
|
||||
("sh", "-c")
|
||||
} else {
|
||||
("cmd.exe", "/c")
|
||||
};
|
||||
|
||||
match Command::new(cmd)
|
||||
.args([arg, script])
|
||||
.stdin(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
{
|
||||
Ok(output) => {
|
||||
let status = output.status;
|
||||
|
||||
let es = match status.code() {
|
||||
Some(code) => code,
|
||||
_ => {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
status.signal().unwrap()
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
return Err(anyhow!("Unknown exit status"));
|
||||
}
|
||||
}
|
||||
fn execute_script(mut arguments: Vec<String>) -> Result<String> {
|
||||
debug!("\nScript arguments vec: {:?}", &arguments);
|
||||
let process = Exec::cmd(arguments.remove(0)).args(&arguments);
|
||||
match process.capture() {
|
||||
Ok(c) => {
|
||||
let es = match c.exit_status {
|
||||
ExitStatus::Exited(c) => c.try_into().unwrap(),
|
||||
ExitStatus::Signaled(c) => c.into(),
|
||||
ExitStatus::Other(c) => c,
|
||||
ExitStatus::Undetermined => -1,
|
||||
};
|
||||
|
||||
if es != 0 {
|
||||
return Err(anyhow!("Exit code = {}", es));
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
|
||||
Ok(c.stdout_str())
|
||||
}
|
||||
Err(error) => {
|
||||
debug!("Command error {error}",);
|
||||
debug!("Command error {}", error.to_string());
|
||||
Err(anyhow!(error.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_scripts(path: PathBuf) -> Result<Vec<PathBuf>> {
|
||||
pub fn find_scripts(mut path: PathBuf) -> Result<Vec<PathBuf>> {
|
||||
path.push(".rustscan_scripts");
|
||||
if path.is_dir() {
|
||||
debug!("Scripts folder found {}", path.display());
|
||||
debug!("Scripts folder found {}", &path.display());
|
||||
let mut files_vec: Vec<PathBuf> = Vec::new();
|
||||
for entry in fs::read_dir(path)? {
|
||||
let entry = entry?;
|
||||
@@ -360,20 +304,20 @@ impl ScriptFile {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug!("Failed to read file: {}", real_path.display());
|
||||
debug!("Failed to read file: {}", &real_path.display());
|
||||
return None;
|
||||
}
|
||||
debug!("ScriptFile {} lines\n{}", real_path.display(), lines_buf);
|
||||
debug!("ScriptFile {} lines\n{}", &real_path.display(), &lines_buf);
|
||||
|
||||
match toml::from_str::<ScriptFile>(&lines_buf) {
|
||||
Ok(mut parsed) => {
|
||||
debug!("Parsed ScriptFile{} \n{:?}", real_path.display(), parsed);
|
||||
debug!("Parsed ScriptFile{} \n{:?}", &real_path.display(), &parsed);
|
||||
parsed.path = Some(real_path);
|
||||
// parsed_scripts.push(parsed);
|
||||
Some(parsed)
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("Failed to parse ScriptFile headers {e}");
|
||||
debug!("Failed to parse ScriptFile headers {}", e.to_string());
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -385,7 +329,6 @@ pub struct ScriptConfig {
|
||||
pub tags: Option<Vec<String>>,
|
||||
pub ports: Option<Vec<String>>,
|
||||
pub developer: Option<Vec<String>>,
|
||||
pub directory: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
@@ -404,11 +347,25 @@ impl ScriptConfig {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use super::{find_scripts, parse_scripts, Script, ScriptFile};
|
||||
|
||||
// Function for testing only, it inserts static values into ip and open_ports
|
||||
// Doesn't use impl in case it's implemented in the super module at some point
|
||||
fn into_script(script_f: ScriptFile) -> Script {
|
||||
Script::build(
|
||||
script_f.path,
|
||||
"127.0.0.1".parse().unwrap(),
|
||||
vec![80, 8080],
|
||||
script_f.port,
|
||||
script_f.ports_separator,
|
||||
script_f.tags,
|
||||
script_f.call_format,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_and_parse_scripts() {
|
||||
let scripts = find_scripts("fixtures/.rustscan_scripts".into()).unwrap();
|
||||
let scripts = find_scripts("fixtures/".into()).unwrap();
|
||||
let scripts = parse_scripts(scripts);
|
||||
assert_eq!(scripts.len(), 4);
|
||||
}
|
||||
@@ -426,6 +383,26 @@ mod tests {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn open_script_file_invalid_call_format() {
|
||||
let mut script_f =
|
||||
ScriptFile::new("fixtures/.rustscan_scripts/test_script.txt".into()).unwrap();
|
||||
script_f.call_format = Some("qwertyuiop".to_string());
|
||||
let script: Script = into_script(script_f);
|
||||
let _output = script.run().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn open_script_file_missing_call_format() {
|
||||
let mut script_f =
|
||||
ScriptFile::new("fixtures/.rustscan_scripts/test_script.txt".into()).unwrap();
|
||||
script_f.call_format = None;
|
||||
let script: Script = into_script(script_f);
|
||||
let _output = script.run().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn open_nonexisting_script_file() {
|
||||
@@ -455,47 +432,34 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_directory_config() {
|
||||
// Create test config
|
||||
let config_str = r#"
|
||||
tags = ["core_approved", "example"]
|
||||
directory = "fixtures/.rustscan_scripts"
|
||||
"#;
|
||||
|
||||
let config: ScriptConfig = toml::from_str(config_str).unwrap();
|
||||
assert_eq!(
|
||||
config.directory,
|
||||
Some("fixtures/.rustscan_scripts".to_string())
|
||||
);
|
||||
|
||||
// Test that the directory is actually used
|
||||
let script_dir_base = PathBuf::from(config.directory.unwrap());
|
||||
let scripts = find_scripts(script_dir_base).unwrap();
|
||||
|
||||
// Verify we found the test script
|
||||
assert!(scripts.iter().any(|p| p
|
||||
.file_name()
|
||||
.and_then(|f| f.to_str())
|
||||
.map(|s| s == "test_script.txt")
|
||||
.unwrap_or(false)));
|
||||
#[cfg(unix)]
|
||||
fn run_bash_script() {
|
||||
let script_f = ScriptFile::new("fixtures/.rustscan_scripts/test_script.sh".into()).unwrap();
|
||||
let script: Script = into_script(script_f);
|
||||
let output = script.run().unwrap();
|
||||
// output has a newline at the end by default, .trim() trims it
|
||||
assert_eq!(output.trim(), "127.0.0.1 80,8080");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_directory_fallback() {
|
||||
let config_str = r#"
|
||||
tags = ["core_approved", "example"]
|
||||
"#;
|
||||
fn run_python_script() {
|
||||
let script_f = ScriptFile::new("fixtures/.rustscan_scripts/test_script.py".into()).unwrap();
|
||||
let script: Script = into_script(script_f);
|
||||
let output = script.run().unwrap();
|
||||
// output has a newline at the end by default, .trim() trims it
|
||||
assert_eq!(
|
||||
output.trim(),
|
||||
"Python script ran with arguments ['fixtures/.rustscan_scripts/test_script.py', '127.0.0.1', '80,8080']"
|
||||
);
|
||||
}
|
||||
|
||||
let config: ScriptConfig = toml::from_str(config_str).unwrap();
|
||||
assert_eq!(config.directory, None);
|
||||
|
||||
// Test fallback to home directory
|
||||
let script_dir_base = if let Some(config_directory) = &config.directory {
|
||||
PathBuf::from(config_directory)
|
||||
} else {
|
||||
dirs::home_dir().unwrap()
|
||||
};
|
||||
|
||||
assert_eq!(script_dir_base, dirs::home_dir().unwrap());
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn run_perl_script() {
|
||||
let script_f = ScriptFile::new("fixtures/.rustscan_scripts/test_script.pl".into()).unwrap();
|
||||
let script: Script = into_script(script_f);
|
||||
let output = script.run().unwrap();
|
||||
// output has a newline at the end by default, .trim() trims it
|
||||
assert_eq!(output.trim(), "Total args passed to fixtures/.rustscan_scripts/test_script.pl : 2\nArg # 1 : 127.0.0.1\nArg # 2 : 80,8080");
|
||||
}
|
||||
}
|
||||
+4
-27
@@ -1,7 +1,6 @@
|
||||
//! Utilities for terminal output during scanning.
|
||||
|
||||
/// Terminal User Interface Module for RustScan
|
||||
/// Defines macros to use
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! warning {
|
||||
($name:expr) => {
|
||||
@@ -43,7 +42,7 @@ macro_rules! output {
|
||||
($name:expr) => {
|
||||
println!(
|
||||
"{} {}",
|
||||
ansi_term::Colour::RGB(0, 255, 9).bold().paint("[>]"),
|
||||
RGansi_term::Colour::RGB(0, 255, 9).bold().paint("[>]"),
|
||||
$name
|
||||
);
|
||||
};
|
||||
@@ -68,7 +67,7 @@ macro_rules! output {
|
||||
macro_rules! funny_opening {
|
||||
// prints a funny quote / opening
|
||||
() => {
|
||||
use rand::seq::IndexedRandom;
|
||||
use rand::seq::SliceRandom;
|
||||
let quotes = vec![
|
||||
"Nmap? More like slowmap.🐢",
|
||||
"🌍HACK THE PLANET🌍",
|
||||
@@ -76,30 +75,8 @@ macro_rules! funny_opening {
|
||||
"Please contribute more quotes to our GitHub https://github.com/rustscan/rustscan",
|
||||
"😵 https://admin.tryhackme.com",
|
||||
"0day was here ♥",
|
||||
"I don't always scan ports, but when I do, I prefer RustScan.",
|
||||
"RustScan: Where scanning meets swagging. 😎",
|
||||
"To scan or not to scan? That is the question.",
|
||||
"RustScan: Because guessing isn't hacking.",
|
||||
"Scanning ports like it's my full-time job. Wait, it is.",
|
||||
"Open ports, closed hearts.",
|
||||
"I scanned my computer so many times, it thinks we're dating.",
|
||||
"Port scanning: Making networking exciting since... whenever.",
|
||||
"You miss 100% of the ports you don't scan. - RustScan",
|
||||
"Breaking and entering... into the world of open ports.",
|
||||
"TCP handshake? More like a friendly high-five!",
|
||||
"Scanning ports: The virtual equivalent of knocking on doors.",
|
||||
"RustScan: Making sure 'closed' isn't just a state of mind.",
|
||||
"RustScan: allowing you to send UDP packets into the void 1200x faster than NMAP",
|
||||
"Port scanning: Because every port has a story to tell.",
|
||||
"I scanned ports so fast, even my computer was surprised.",
|
||||
"Scanning ports faster than you can say 'SYN ACK'",
|
||||
"RustScan: Where '404 Not Found' meets '200 OK'.",
|
||||
"RustScan: Exploring the digital landscape, one IP at a time.",
|
||||
"TreadStone was here 🚀",
|
||||
"With RustScan, I scan ports so fast, even my firewall gets whiplash 💨",
|
||||
"Scanning ports so fast, even the internet got a speeding ticket!",
|
||||
];
|
||||
let random_quote = quotes.choose(&mut rand::rng()).unwrap();
|
||||
let random_quote = quotes.choose(&mut rand::thread_rng()).unwrap();
|
||||
|
||||
println!("{}\n", random_quote);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Test rustscan against different targets with a time limit.
|
||||
* The tests assumes target/debug/rustscan has already been built.
|
||||
*
|
||||
* The tests are #[ignore] to avoid running them during normal development.
|
||||
*
|
||||
* Their tests in the timelimits module are run by travis during CI.
|
||||
*/
|
||||
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
use wait_timeout::ChildExt;
|
||||
|
||||
const TIMEOUT_MARGIN: u32 = 3;
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
fn run_rustscan_with_timeout(args: &[&str], timeout: Duration) {
|
||||
println!("Running: target/debug/rustscan: {}", args.join(" "));
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
let mut child = Command::new("target/debug/rustscan")
|
||||
.args(args)
|
||||
.spawn()
|
||||
.unwrap();
|
||||
|
||||
let mut tries = TIMEOUT_MARGIN;
|
||||
loop {
|
||||
match child.wait_timeout(timeout).unwrap() {
|
||||
Some(_status) => break,
|
||||
None => {
|
||||
tries -= 1;
|
||||
if tries == 0 {
|
||||
// child hasn't exited yet
|
||||
child.kill().unwrap();
|
||||
child.wait().unwrap().code().unwrap();
|
||||
panic!("Timeout while running command");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let end = Instant::now();
|
||||
let duration = end.saturating_duration_since(start).as_secs_f32();
|
||||
|
||||
println!("time: {:1.1}s", duration);
|
||||
}
|
||||
|
||||
mod timelimits {
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn scan_localhost() {
|
||||
let timeout = super::Duration::from_secs(25);
|
||||
super::run_rustscan_with_timeout(&["--greppable", "--no-nmap", "127.0.0.1"], timeout);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn scan_google_com() {
|
||||
super::run_rustscan_with_timeout(
|
||||
&[
|
||||
"--greppable",
|
||||
"--no-nmap",
|
||||
"-u",
|
||||
"5000",
|
||||
"-b",
|
||||
"2500",
|
||||
"google.com",
|
||||
],
|
||||
super::Duration::from_secs(28),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn scan_example_com() {
|
||||
super::run_rustscan_with_timeout(
|
||||
&[
|
||||
"--greppable",
|
||||
"--no-nmap",
|
||||
"-u",
|
||||
"5000",
|
||||
"-b",
|
||||
"2500",
|
||||
"example.com",
|
||||
],
|
||||
super::Duration::from_secs(28),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn scan_rustscan_cmnatic_co_uk() {
|
||||
super::run_rustscan_with_timeout(
|
||||
&[
|
||||
"--greppable",
|
||||
"--no-nmap",
|
||||
"-u",
|
||||
"5000",
|
||||
"-b",
|
||||
"2500",
|
||||
"rustscan.cmnatic.co.uk",
|
||||
],
|
||||
super::Duration::from_secs(26),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user