This commit is contained in:
CarterPerez-dev 2026-05-23 05:01:01 -04:00
parent 481e07bddd
commit 0006ddad3d
105 changed files with 2352 additions and 3114 deletions

15
.clang-format Normal file
View File

@ -0,0 +1,15 @@
# ©AngelaMos | 2026
# .clang-format
BasedOnStyle: LLVM
IndentWidth: 4
TabWidth: 4
UseTab: Never
ColumnLimit: 100
AccessModifierOffset: -4
AllowShortFunctionsOnASingleLine: None
AllowShortIfStatementsOnASingleLine: Never
AllowShortLoopsOnASingleLine: false
SortIncludes: CaseSensitive
PointerAlignment: Left
SpaceBeforeParens: ControlStatements
BreakBeforeBraces: Attach

View File

@ -68,6 +68,16 @@ jobs:
- name: dlp-scanner - name: dlp-scanner
type: ruff type: ruff
path: PROJECTS/intermediate/dlp-scanner path: PROJECTS/intermediate/dlp-scanner
# Python (ruff) - Foundations
- name: hash-identifier
type: ruff
path: PROJECTS/foundations/hash-identifier
- name: http-headers-scanner
type: ruff
path: PROJECTS/foundations/http-headers-scanner
- name: password-manager
type: ruff
path: PROJECTS/foundations/password-manager
# Biome (frontend) # Biome (frontend)
- name: bug-bounty-platform-frontend - name: bug-bounty-platform-frontend
type: biome type: biome
@ -84,6 +94,21 @@ jobs:
- name: encrypted-p2p-chat-frontend - name: encrypted-p2p-chat-frontend
type: biome type: biome
path: PROJECTS/advanced/encrypted-p2p-chat/frontend path: PROJECTS/advanced/encrypted-p2p-chat/frontend
- name: canary-token-generator-frontend
type: biome
path: PROJECTS/beginner/canary-token-generator/frontend
- name: ai-threat-detection-frontend
type: biome
path: PROJECTS/advanced/ai-threat-detection/frontend
- name: binary-analysis-tool-frontend
type: biome
path: PROJECTS/intermediate/binary-analysis-tool/frontend
- name: honeypot-network-frontend
type: biome
path: PROJECTS/advanced/honeypot-network/frontend
- name: monitor-the-situation-dashboard-frontend
type: biome
path: PROJECTS/advanced/monitor-the-situation-dashboard/frontend
# Go # Go
- name: simple-vulnerability-scanner - name: simple-vulnerability-scanner
type: go type: go
@ -94,10 +119,51 @@ jobs:
- name: secrets-scanner - name: secrets-scanner
type: go type: go
path: PROJECTS/intermediate/secrets-scanner path: PROJECTS/intermediate/secrets-scanner
- name: canary-token-generator-backend
type: go
path: PROJECTS/beginner/canary-token-generator/backend
- name: systemd-persistence-scanner
type: go
path: PROJECTS/beginner/systemd-persistence-scanner
- name: sbom-generator-vulnerability-matcher
type: go
path: PROJECTS/intermediate/sbom-generator-vulnerability-matcher
- name: honeypot-network
type: go
path: PROJECTS/advanced/honeypot-network
- name: monitor-the-situation-dashboard-backend
type: go
path: PROJECTS/advanced/monitor-the-situation-dashboard/backend
# Nim # Nim
- name: credential-enumeration - name: credential-enumeration
type: nim type: nim
path: PROJECTS/intermediate/credential-enumeration path: PROJECTS/intermediate/credential-enumeration
# Rust
- name: binary-analysis-tool-backend
type: rust
path: PROJECTS/intermediate/binary-analysis-tool/backend
# Crystal
- name: credential-rotation-enforcer
type: crystal
path: PROJECTS/intermediate/credential-rotation-enforcer
# V (Vlang)
- name: firewall-rule-engine
type: v
path: PROJECTS/beginner/firewall-rule-engine
# C++
- name: hash-cracker
type: cpp
path: PROJECTS/beginner/hash-cracker
- name: simple-port-scanner
type: cpp
path: PROJECTS/beginner/simple-port-scanner
- name: network-traffic-analyzer-cpp
type: cpp
path: PROJECTS/beginner/network-traffic-analyzer/cpp
# Bash (shellcheck)
- name: linux-cis-hardening-auditor
type: bash
path: PROJECTS/beginner/linux-cis-hardening-auditor
defaults: defaults:
run: run:
@ -168,6 +234,61 @@ jobs:
if: matrix.type == 'nim' if: matrix.type == 'nim'
run: nimble install -y nph run: nimble install -y nph
# Rust Setup
- name: Install Rust stable
if: matrix.type == 'rust'
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Cache Cargo registry and target
if: matrix.type == 'rust'
uses: Swatinem/rust-cache@v2
with:
workspaces: "${{ matrix.path }} -> target"
# Crystal Setup
- name: Install Crystal
if: matrix.type == 'crystal'
uses: crystal-lang/install-crystal@v1
with:
crystal: latest
- name: Cache shards
if: matrix.type == 'crystal'
uses: actions/cache@v4
with:
path: ${{ matrix.path }}/lib
key: ${{ runner.os }}-shards-${{ matrix.name }}-${{ hashFiles(format('{0}/shard.lock', matrix.path)) }}
restore-keys: |
${{ runner.os }}-shards-${{ matrix.name }}-
${{ runner.os }}-shards-
- name: Install shards
if: matrix.type == 'crystal'
run: shards install
# V (Vlang) Setup
- name: Install V
if: matrix.type == 'v'
uses: vlang/setup-v@v1.4
with:
check-latest: true
# C++ Setup
- name: Install clang-format and cppcheck
if: matrix.type == 'cpp'
run: |
sudo apt-get update -qq
sudo apt-get install -y clang-format cppcheck
# Bash Setup (shellcheck pre-installed on ubuntu-latest, but ensure latest)
- name: Install shellcheck
if: matrix.type == 'bash'
run: |
sudo apt-get update -qq
sudo apt-get install -y shellcheck
# Ruff Linting # Ruff Linting
- name: Run ruff - name: Run ruff
if: matrix.type == 'ruff' if: matrix.type == 'ruff'
@ -247,6 +368,152 @@ jobs:
cat nim-output.txt cat nim-output.txt
continue-on-error: true continue-on-error: true
# Rust Linting
- name: Run cargo fmt and clippy
if: matrix.type == 'rust'
id: rust
run: |
RUST_OK=true
echo "Running cargo fmt --check..."
if cargo fmt -- --check > rust-output.txt 2>&1; then
echo "cargo fmt: no issues"
else
RUST_OK=false
echo "cargo fmt: formatting issues found"
fi
echo "Running cargo clippy..."
if cargo clippy --all-targets --all-features -- -D warnings >> rust-output.txt 2>&1; then
echo "cargo clippy: no issues"
else
RUST_OK=false
echo "cargo clippy: issues found"
fi
if [[ "$RUST_OK" == "true" ]]; then
echo "RUST_PASSED=true" >> $GITHUB_ENV
else
echo "RUST_PASSED=false" >> $GITHUB_ENV
fi
cat rust-output.txt
continue-on-error: true
# Crystal Linting
- name: Run crystal format + ameba + compile check
if: matrix.type == 'crystal'
id: crystal
run: |
CRYSTAL_OK=true
echo "Running crystal tool format --check..."
if crystal tool format --check src/ spec/ > crystal-output.txt 2>&1; then
echo "crystal format: no issues"
else
CRYSTAL_OK=false
echo "crystal format: formatting issues found"
fi
echo "Running ameba..."
if bin/ameba src/ >> crystal-output.txt 2>&1; then
echo "ameba: no issues"
else
CRYSTAL_OK=false
echo "ameba: issues found"
fi
echo "Running crystal build --no-codegen..."
if crystal build --no-codegen src/cre.cr >> crystal-output.txt 2>&1; then
echo "crystal build: passed"
else
CRYSTAL_OK=false
echo "crystal build: failed"
fi
if [[ "$CRYSTAL_OK" == "true" ]]; then
echo "CRYSTAL_PASSED=true" >> $GITHUB_ENV
else
echo "CRYSTAL_PASSED=false" >> $GITHUB_ENV
fi
cat crystal-output.txt
continue-on-error: true
# V Linting
- name: Run v fmt and v vet
if: matrix.type == 'v'
id: v
run: |
V_OK=true
echo "Running v fmt -diff src/..."
v fmt -diff src/ > v-output.txt 2>&1 || true
echo "Running v fmt -verify src/..."
if v fmt -verify src/ >> v-output.txt 2>&1; then
echo "v fmt: no issues"
else
V_OK=false
echo "v fmt: formatting issues found"
fi
echo "Running v vet src/..."
if v vet src/ >> v-output.txt 2>&1; then
echo "v vet: no issues"
else
V_OK=false
echo "v vet: issues found"
fi
if [[ "$V_OK" == "true" ]]; then
echo "V_PASSED=true" >> $GITHUB_ENV
else
echo "V_PASSED=false" >> $GITHUB_ENV
fi
cat v-output.txt
continue-on-error: true
# C++ Linting
- name: Run clang-format and cppcheck
if: matrix.type == 'cpp'
id: cpp
run: |
CPP_OK=true
echo "Running clang-format check..."
FILES=$(find . \( -path ./build -o -path ./cmake-build -o -path ./node_modules -o -path ./.git \) -prune -o \( -name '*.cpp' -o -name '*.hpp' -o -name '*.h' \) -print 2>/dev/null)
if [ -n "$FILES" ]; then
if echo "$FILES" | xargs clang-format --dry-run --Werror > cpp-output.txt 2>&1; then
echo "clang-format: no issues"
else
CPP_OK=false
echo "clang-format: formatting issues found"
fi
else
echo "clang-format: no C++ files found" > cpp-output.txt
fi
echo "Running cppcheck..."
if cppcheck --enable=warning,style,performance,portability --error-exitcode=1 --suppress=missingIncludeSystem --suppress=normalCheckLevelMaxBranches -i build -i cmake-build -q . >> cpp-output.txt 2>&1; then
echo "cppcheck: no issues"
else
CPP_OK=false
echo "cppcheck: issues found"
fi
if [[ "$CPP_OK" == "true" ]]; then
echo "CPP_PASSED=true" >> $GITHUB_ENV
else
echo "CPP_PASSED=false" >> $GITHUB_ENV
fi
cat cpp-output.txt
continue-on-error: true
# Bash Linting
- name: Run shellcheck
if: matrix.type == 'bash'
id: bash
run: |
echo "Running shellcheck..."
FILES=$(find . -name '*.sh' -not -path '*/.venv/*' -not -path '*/node_modules/*' 2>/dev/null || true)
if [ -z "$FILES" ]; then
echo "shellcheck: no .sh files found" > bash-output.txt
echo "BASH_PASSED=true" >> $GITHUB_ENV
elif echo "$FILES" | xargs shellcheck > bash-output.txt 2>&1; then
echo "BASH_PASSED=true" >> $GITHUB_ENV
echo "shellcheck: no issues"
else
echo "BASH_PASSED=false" >> $GITHUB_ENV
echo "shellcheck: issues found"
fi
cat bash-output.txt
continue-on-error: true
# Create Summary for Ruff # Create Summary for Ruff
- name: Create Ruff Lint Summary - name: Create Ruff Lint Summary
if: matrix.type == 'ruff' if: matrix.type == 'ruff'
@ -371,6 +638,151 @@ jobs:
fi fi
} >> $GITHUB_STEP_SUMMARY } >> $GITHUB_STEP_SUMMARY
# Create Summary for Rust
- name: Create Rust Lint Summary
if: matrix.type == 'rust'
run: |
{
echo "## Lint Results: ${{ matrix.name }}"
echo ''
if [[ "${{ env.RUST_PASSED }}" == "true" ]]; then
echo '### cargo fmt + clippy: **Passed**'
echo 'No Rust issues found.'
else
echo '### cargo fmt + clippy: **Issues Found**'
echo '<details><summary>View output</summary>'
echo ''
echo '```'
head -100 rust-output.txt
echo '```'
echo '</details>'
fi
echo ''
if [[ "${{ env.RUST_PASSED }}" == "true" ]]; then
echo '---'
echo '### All checks passed!'
else
echo '---'
echo '### Review the issues above'
fi
} >> $GITHUB_STEP_SUMMARY
# Create Summary for Crystal
- name: Create Crystal Lint Summary
if: matrix.type == 'crystal'
run: |
{
echo "## Lint Results: ${{ matrix.name }}"
echo ''
if [[ "${{ env.CRYSTAL_PASSED }}" == "true" ]]; then
echo '### crystal format + ameba + build: **Passed**'
echo 'No Crystal issues found.'
else
echo '### crystal format + ameba + build: **Issues Found**'
echo '<details><summary>View output</summary>'
echo ''
echo '```'
head -100 crystal-output.txt
echo '```'
echo '</details>'
fi
echo ''
if [[ "${{ env.CRYSTAL_PASSED }}" == "true" ]]; then
echo '---'
echo '### All checks passed!'
else
echo '---'
echo '### Review the issues above'
fi
} >> $GITHUB_STEP_SUMMARY
# Create Summary for V
- name: Create V Lint Summary
if: matrix.type == 'v'
run: |
{
echo "## Lint Results: ${{ matrix.name }}"
echo ''
if [[ "${{ env.V_PASSED }}" == "true" ]]; then
echo '### v fmt + v vet: **Passed**'
echo 'No V issues found.'
else
echo '### v fmt + v vet: **Issues Found**'
echo '<details><summary>View output</summary>'
echo ''
echo '```'
head -100 v-output.txt
echo '```'
echo '</details>'
fi
echo ''
if [[ "${{ env.V_PASSED }}" == "true" ]]; then
echo '---'
echo '### All checks passed!'
else
echo '---'
echo '### Review the issues above'
fi
} >> $GITHUB_STEP_SUMMARY
# Create Summary for C++
- name: Create C++ Lint Summary
if: matrix.type == 'cpp'
run: |
{
echo "## Lint Results: ${{ matrix.name }}"
echo ''
if [[ "${{ env.CPP_PASSED }}" == "true" ]]; then
echo '### clang-format + cppcheck: **Passed**'
echo 'No C++ issues found.'
else
echo '### clang-format + cppcheck: **Issues Found**'
echo '<details><summary>View output</summary>'
echo ''
echo '```'
head -100 cpp-output.txt
echo '```'
echo '</details>'
fi
echo ''
if [[ "${{ env.CPP_PASSED }}" == "true" ]]; then
echo '---'
echo '### All checks passed!'
else
echo '---'
echo '### Review the issues above'
fi
} >> $GITHUB_STEP_SUMMARY
# Create Summary for Bash
- name: Create Bash Lint Summary
if: matrix.type == 'bash'
run: |
{
echo "## Lint Results: ${{ matrix.name }}"
echo ''
if [[ "${{ env.BASH_PASSED }}" == "true" ]]; then
echo '### shellcheck: **Passed**'
echo 'No shell issues found.'
else
echo '### shellcheck: **Issues Found**'
echo '<details><summary>View output</summary>'
echo ''
echo '```'
head -100 bash-output.txt
echo '```'
echo '</details>'
fi
echo ''
if [[ "${{ env.BASH_PASSED }}" == "true" ]]; then
echo '---'
echo '### All checks passed!'
else
echo '---'
echo '### Review the issues above'
fi
} >> $GITHUB_STEP_SUMMARY
# Exit with proper status # Exit with proper status
- name: Check lint status - name: Check lint status
run: | run: |
@ -394,5 +806,30 @@ jobs:
echo "Nim lint checks failed" echo "Nim lint checks failed"
exit 1 exit 1
fi fi
elif [[ "${{ matrix.type }}" == "rust" ]]; then
if [[ "${{ env.RUST_PASSED }}" == "false" ]]; then
echo "Rust lint checks failed"
exit 1
fi
elif [[ "${{ matrix.type }}" == "crystal" ]]; then
if [[ "${{ env.CRYSTAL_PASSED }}" == "false" ]]; then
echo "Crystal lint checks failed"
exit 1
fi
elif [[ "${{ matrix.type }}" == "v" ]]; then
if [[ "${{ env.V_PASSED }}" == "false" ]]; then
echo "V lint checks failed"
exit 1
fi
elif [[ "${{ matrix.type }}" == "cpp" ]]; then
if [[ "${{ env.CPP_PASSED }}" == "false" ]]; then
echo "C++ lint checks failed"
exit 1
fi
elif [[ "${{ matrix.type }}" == "bash" ]]; then
if [[ "${{ env.BASH_PASSED }}" == "false" ]]; then
echo "Bash lint checks failed"
exit 1
fi
fi fi
echo "All lint checks passed" echo "All lint checks passed"

View File

@ -84,6 +84,37 @@ repos:
files: ^PROJECTS/advanced/ai-threat-detection/backend/ files: ^PROJECTS/advanced/ai-threat-detection/backend/
exclude: (\.venv|__pycache__|\.pytest_cache)/ exclude: (\.venv|__pycache__|\.pytest_cache)/
- id: ruff
name: ruff check (dlp-scanner)
args: [--fix, --exit-non-zero-on-fix]
files: ^PROJECTS/intermediate/dlp-scanner/
exclude: (\.venv|__pycache__|\.pytest_cache)/
- id: ruff
name: ruff check (linux-ebpf-security-tracer)
args: [--fix, --exit-non-zero-on-fix]
files: ^PROJECTS/beginner/linux-ebpf-security-tracer/
exclude: (\.venv|__pycache__|\.pytest_cache)/
# Foundations projects
- id: ruff
name: ruff check (hash-identifier)
args: [--fix, --exit-non-zero-on-fix]
files: ^PROJECTS/foundations/hash-identifier/
exclude: (\.venv|__pycache__|\.pytest_cache)/
- id: ruff
name: ruff check (http-headers-scanner)
args: [--fix, --exit-non-zero-on-fix]
files: ^PROJECTS/foundations/http-headers-scanner/
exclude: (\.venv|__pycache__|\.pytest_cache)/
- id: ruff
name: ruff check (password-manager)
args: [--fix, --exit-non-zero-on-fix]
files: ^PROJECTS/foundations/password-manager/
exclude: (\.venv|__pycache__|\.pytest_cache)/
# Go golangci-lint Checks # Go golangci-lint Checks
- repo: local - repo: local
@ -112,6 +143,48 @@ repos:
types: [go] types: [go]
pass_filenames: false pass_filenames: false
- id: golangci-lint-canary-token-generator
name: golangci-lint (canary-token-generator backend)
entry: bash -c 'cd PROJECTS/beginner/canary-token-generator/backend && golangci-lint run --fix'
language: system
files: ^PROJECTS/beginner/canary-token-generator/backend/
types: [go]
pass_filenames: false
- id: golangci-lint-systemd-persistence-scanner
name: golangci-lint (systemd-persistence-scanner)
entry: bash -c 'cd PROJECTS/beginner/systemd-persistence-scanner && golangci-lint run --fix'
language: system
files: ^PROJECTS/beginner/systemd-persistence-scanner/
types: [go]
pass_filenames: false
- id: golangci-lint-sbom-generator
name: golangci-lint (sbom-generator-vulnerability-matcher)
entry: bash -c 'cd PROJECTS/intermediate/sbom-generator-vulnerability-matcher && golangci-lint run --fix'
language: system
files: ^PROJECTS/intermediate/sbom-generator-vulnerability-matcher/
exclude: ^PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/
types: [go]
pass_filenames: false
- id: golangci-lint-honeypot-network
name: golangci-lint (honeypot-network)
entry: bash -c 'cd PROJECTS/advanced/honeypot-network && golangci-lint run --fix'
language: system
files: ^PROJECTS/advanced/honeypot-network/
exclude: ^PROJECTS/advanced/honeypot-network/frontend/
types: [go]
pass_filenames: false
- id: golangci-lint-monitor-the-situation-dashboard
name: golangci-lint (monitor-the-situation-dashboard backend)
entry: bash -c 'cd PROJECTS/advanced/monitor-the-situation-dashboard/backend && golangci-lint run --fix'
language: system
files: ^PROJECTS/advanced/monitor-the-situation-dashboard/backend/
types: [go]
pass_filenames: false
# Biome Frontend Checks # Biome Frontend Checks
- repo: local - repo: local
hooks: hooks:
@ -150,6 +223,41 @@ repos:
files: ^PROJECTS/advanced/encrypted-p2p-chat/frontend/src/ files: ^PROJECTS/advanced/encrypted-p2p-chat/frontend/src/
pass_filenames: false pass_filenames: false
- id: biome-canary-token-generator
name: biome check (canary-token-generator frontend)
entry: bash -c 'cd PROJECTS/beginner/canary-token-generator/frontend && npx @biomejs/biome check .'
language: system
files: ^PROJECTS/beginner/canary-token-generator/frontend/src/
pass_filenames: false
- id: biome-ai-threat-detection
name: biome check (ai-threat-detection frontend)
entry: bash -c 'cd PROJECTS/advanced/ai-threat-detection/frontend && npx @biomejs/biome check .'
language: system
files: ^PROJECTS/advanced/ai-threat-detection/frontend/src/
pass_filenames: false
- id: biome-binary-analysis-tool
name: biome check (binary-analysis-tool frontend)
entry: bash -c 'cd PROJECTS/intermediate/binary-analysis-tool/frontend && npx @biomejs/biome check .'
language: system
files: ^PROJECTS/intermediate/binary-analysis-tool/frontend/src/
pass_filenames: false
- id: biome-honeypot-network
name: biome check (honeypot-network frontend)
entry: bash -c 'cd PROJECTS/advanced/honeypot-network/frontend && npx @biomejs/biome check .'
language: system
files: ^PROJECTS/advanced/honeypot-network/frontend/src/
pass_filenames: false
- id: biome-monitor-the-situation-dashboard
name: biome check (monitor-the-situation-dashboard frontend)
entry: bash -c 'cd PROJECTS/advanced/monitor-the-situation-dashboard/frontend && npx @biomejs/biome check .'
language: system
files: ^PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/
pass_filenames: false
# Nim nph Checks # Nim nph Checks
- repo: local - repo: local
hooks: hooks:
@ -160,6 +268,130 @@ repos:
files: ^PROJECTS/intermediate/credential-enumeration/src/ files: ^PROJECTS/intermediate/credential-enumeration/src/
pass_filenames: false pass_filenames: false
# Rust Checks (cargo fmt + clippy)
- repo: local
hooks:
- id: cargo-fmt-binary-analysis-tool
name: cargo fmt (binary-analysis-tool backend)
entry: bash -c 'cd PROJECTS/intermediate/binary-analysis-tool/backend && cargo fmt -- --check'
language: system
files: ^PROJECTS/intermediate/binary-analysis-tool/backend/
types: [rust]
pass_filenames: false
- id: cargo-clippy-binary-analysis-tool
name: cargo clippy (binary-analysis-tool backend)
entry: bash -c 'cd PROJECTS/intermediate/binary-analysis-tool/backend && cargo clippy --all-targets --all-features -- -D warnings'
language: system
files: ^PROJECTS/intermediate/binary-analysis-tool/backend/
types: [rust]
pass_filenames: false
# Crystal Checks (format + ameba + compile)
- repo: local
hooks:
- id: crystal-format-credential-rotation-enforcer
name: crystal format check (credential-rotation-enforcer)
entry: bash -c 'cd PROJECTS/intermediate/credential-rotation-enforcer && crystal tool format --check src/ spec/'
language: system
files: ^PROJECTS/intermediate/credential-rotation-enforcer/
types_or: [crystal]
pass_filenames: false
- id: ameba-credential-rotation-enforcer
name: ameba (credential-rotation-enforcer)
entry: bash -c 'cd PROJECTS/intermediate/credential-rotation-enforcer && bin/ameba src/'
language: system
files: ^PROJECTS/intermediate/credential-rotation-enforcer/
types_or: [crystal]
pass_filenames: false
- id: crystal-build-check-credential-rotation-enforcer
name: crystal compile check (credential-rotation-enforcer)
entry: bash -c 'cd PROJECTS/intermediate/credential-rotation-enforcer && crystal build --no-codegen src/cre.cr'
language: system
files: ^PROJECTS/intermediate/credential-rotation-enforcer/
types_or: [crystal]
pass_filenames: false
# V (Vlang) Checks
- repo: local
hooks:
- id: v-fmt-firewall-rule-engine
name: v fmt (firewall-rule-engine)
entry: bash -c 'cd PROJECTS/beginner/firewall-rule-engine && v fmt -verify src/'
language: system
files: ^PROJECTS/beginner/firewall-rule-engine/src/
pass_filenames: false
- id: v-vet-firewall-rule-engine
name: v vet (firewall-rule-engine)
entry: bash -c 'cd PROJECTS/beginner/firewall-rule-engine && v vet src/'
language: system
files: ^PROJECTS/beginner/firewall-rule-engine/src/
pass_filenames: false
# C++ Checks (clang-format + cppcheck)
- repo: local
hooks:
- id: clang-format-hash-cracker
name: clang-format (hash-cracker)
entry: bash -c 'cd PROJECTS/beginner/hash-cracker && find . \( -path ./build -o -path ./cmake-build -o -path ./.git \) -prune -o \( -name "*.cpp" -o -name "*.hpp" -o -name "*.h" \) -print 2>/dev/null | xargs -r clang-format --dry-run --Werror'
language: system
files: ^PROJECTS/beginner/hash-cracker/
types_or: [c++, c]
pass_filenames: false
- id: cppcheck-hash-cracker
name: cppcheck (hash-cracker)
entry: bash -c 'cd PROJECTS/beginner/hash-cracker && cppcheck --enable=warning,style,performance,portability --error-exitcode=1 --suppress=missingIncludeSystem --suppress=normalCheckLevelMaxBranches -i build -i cmake-build -q .'
language: system
files: ^PROJECTS/beginner/hash-cracker/
types_or: [c++, c]
pass_filenames: false
- id: clang-format-simple-port-scanner
name: clang-format (simple-port-scanner)
entry: bash -c 'cd PROJECTS/beginner/simple-port-scanner && find . \( -path ./build -o -path ./cmake-build -o -path ./.git \) -prune -o \( -name "*.cpp" -o -name "*.hpp" -o -name "*.h" \) -print 2>/dev/null | xargs -r clang-format --dry-run --Werror'
language: system
files: ^PROJECTS/beginner/simple-port-scanner/
types_or: [c++, c]
pass_filenames: false
- id: cppcheck-simple-port-scanner
name: cppcheck (simple-port-scanner)
entry: bash -c 'cd PROJECTS/beginner/simple-port-scanner && cppcheck --enable=warning,style,performance,portability --error-exitcode=1 --suppress=missingIncludeSystem --suppress=normalCheckLevelMaxBranches -i build -i cmake-build -q .'
language: system
files: ^PROJECTS/beginner/simple-port-scanner/
types_or: [c++, c]
pass_filenames: false
- id: clang-format-network-traffic-analyzer-cpp
name: clang-format (network-traffic-analyzer/cpp)
entry: bash -c 'cd PROJECTS/beginner/network-traffic-analyzer/cpp && find . \( -path ./build -o -path ./cmake-build -o -path ./.git \) -prune -o \( -name "*.cpp" -o -name "*.hpp" -o -name "*.h" \) -print 2>/dev/null | xargs -r clang-format --dry-run --Werror'
language: system
files: ^PROJECTS/beginner/network-traffic-analyzer/cpp/
types_or: [c++, c]
pass_filenames: false
- id: cppcheck-network-traffic-analyzer-cpp
name: cppcheck (network-traffic-analyzer/cpp)
entry: bash -c 'cd PROJECTS/beginner/network-traffic-analyzer/cpp && cppcheck --enable=warning,style,performance,portability --error-exitcode=1 --suppress=missingIncludeSystem --suppress=normalCheckLevelMaxBranches -i build -i cmake-build -q .'
language: system
files: ^PROJECTS/beginner/network-traffic-analyzer/cpp/
types_or: [c++, c]
pass_filenames: false
# Bash (shellcheck)
- repo: local
hooks:
- id: shellcheck-linux-cis-hardening-auditor
name: shellcheck (linux-cis-hardening-auditor)
entry: bash -c 'find PROJECTS/beginner/linux-cis-hardening-auditor -name "*.sh" -not -path "*/.venv/*" | xargs -r shellcheck'
language: system
files: ^PROJECTS/beginner/linux-cis-hardening-auditor/.*\.sh$
pass_filenames: false
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0 rev: v6.0.0
hooks: hooks:

View File

@ -2,8 +2,8 @@
// event-feed.tsx // event-feed.tsx
import { useWebSocketStore } from '@/core/lib/websocket.store' import { useWebSocketStore } from '@/core/lib/websocket.store'
import { ServiceBadge } from './service-badge'
import styles from './event-feed.module.scss' import styles from './event-feed.module.scss'
import { ServiceBadge } from './service-badge'
const MAX_VISIBLE = 15 const MAX_VISIBLE = 15

View File

@ -9,9 +9,5 @@ interface ServiceBadgeProps {
} }
export function ServiceBadge({ service }: ServiceBadgeProps) { export function ServiceBadge({ service }: ServiceBadgeProps) {
return ( return <span className={styles.badge}>{service.toUpperCase()}</span>
<span className={styles.badge}>
{service.toUpperCase()}
</span>
)
} }

View File

@ -52,9 +52,7 @@ export function Shell() {
<div className={styles.status}> <div className={styles.status}>
<span className={styles.statusLabel}>LINK</span> <span className={styles.statusLabel}>LINK</span>
<span <span className={connected ? styles.statusOn : styles.statusOff}>
className={connected ? styles.statusOn : styles.statusOff}
>
{connected ? 'ACTIVE' : 'DOWN'} {connected ? 'ACTIVE' : 'DOWN'}
</span> </span>
</div> </div>

View File

@ -57,9 +57,7 @@ export function AttackerDetailPage() {
<div className={styles.dossierRow}> <div className={styles.dossierRow}>
<span className={styles.dossierLabel}>THREAT SCORE</span> <span className={styles.dossierLabel}>THREAT SCORE</span>
<span <span className={`${styles.dossierValue} ${styles.threatValue}`}>
className={`${styles.dossierValue} ${styles.threatValue}`}
>
{attacker.threat_score} {attacker.threat_score}
</span> </span>
</div> </div>

View File

@ -34,10 +34,7 @@ export function AttackersPage() {
{attackers?.map((a) => ( {attackers?.map((a) => (
<tr key={a.id}> <tr key={a.id}>
<td> <td>
<Link <Link to={`/attackers/${a.id}`} className={styles.link}>
to={`/attackers/${a.id}`}
className={styles.link}
>
{a.ip} {a.ip}
</Link> </Link>
</td> </td>

View File

@ -113,11 +113,7 @@ export function DashboardPage() {
axisLine={false} axisLine={false}
/> />
<Tooltip contentStyle={TOOLTIP_STYLE} cursor={false} /> <Tooltip contentStyle={TOOLTIP_STYLE} cursor={false} />
<Bar <Bar dataKey="count" fill="oklch(70% 0.19 55)" radius={0} />
dataKey="count"
fill="oklch(70% 0.19 55)"
radius={0}
/>
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
@ -143,28 +139,24 @@ export function DashboardPage() {
<section className={styles.panel}> <section className={styles.panel}>
<h2 className={styles.panelLabel}>Top Usernames</h2> <h2 className={styles.panelLabel}>Top Usernames</h2>
<div className={styles.list}> <div className={styles.list}>
{credentials.top_usernames {credentials.top_usernames.slice(0, TOP_CREDS_LIMIT).map((u) => (
.slice(0, TOP_CREDS_LIMIT) <div key={u.value} className={styles.listRow}>
.map((u) => ( <span className={styles.listLabel}>{u.value}</span>
<div key={u.value} className={styles.listRow}> <span className={styles.listValue}>{u.count}</span>
<span className={styles.listLabel}>{u.value}</span> </div>
<span className={styles.listValue}>{u.count}</span> ))}
</div>
))}
</div> </div>
</section> </section>
<section className={styles.panel}> <section className={styles.panel}>
<h2 className={styles.panelLabel}>Top Passwords</h2> <h2 className={styles.panelLabel}>Top Passwords</h2>
<div className={styles.list}> <div className={styles.list}>
{credentials.top_passwords {credentials.top_passwords.slice(0, TOP_CREDS_LIMIT).map((p) => (
.slice(0, TOP_CREDS_LIMIT) <div key={p.value} className={styles.listRow}>
.map((p) => ( <span className={styles.listLabel}>{p.value}</span>
<div key={p.value} className={styles.listRow}> <span className={styles.listValue}>{p.count}</span>
<span className={styles.listLabel}>{p.value}</span> </div>
<span className={styles.listValue}>{p.count}</span> ))}
</div>
))}
</div> </div>
</section> </section>
</div> </div>

View File

@ -10,10 +10,7 @@ const PAGE_SIZE = 50
export function EventsPage() { export function EventsPage() {
const [ipFilter, setIpFilter] = useState('') const [ipFilter, setIpFilter] = useState('')
const { data: events, isLoading } = useEvents( const { data: events, isLoading } = useEvents(PAGE_SIZE, ipFilter || undefined)
PAGE_SIZE,
ipFilter || undefined
)
return ( return (
<div className={styles.page}> <div className={styles.page}>

View File

@ -27,24 +27,16 @@ export function IntelPage() {
const res = await apiClient.get(API_ENDPOINTS.IOCS.EXPORT_STIX, { const res = await apiClient.get(API_ENDPOINTS.IOCS.EXPORT_STIX, {
responseType: 'text', responseType: 'text',
}) })
downloadBlob( downloadBlob(res.data as string, 'hive-iocs.stix.json', 'application/json')
res.data as string,
'hive-iocs.stix.json',
'application/json'
)
} }
async function exportBlocklist(format: string) { async function exportBlocklist(format: string) {
const res = await apiClient.get( const res = await apiClient.get(API_ENDPOINTS.IOCS.EXPORT_BLOCKLIST, {
API_ENDPOINTS.IOCS.EXPORT_BLOCKLIST, params: { format },
{ params: { format }, responseType: 'text' } responseType: 'text',
) })
const ext = format === 'csv' ? '.csv' : '.txt' const ext = format === 'csv' ? '.csv' : '.txt'
downloadBlob( downloadBlob(res.data as string, `hive-blocklist${ext}`, 'text/plain')
res.data as string,
`hive-blocklist${ext}`,
'text/plain'
)
} }
return ( return (
@ -52,18 +44,12 @@ export function IntelPage() {
<header className={styles.heading}> <header className={styles.heading}>
<div className={styles.headingLeft}> <div className={styles.headingLeft}>
<h1 className={styles.title}>Intel</h1> <h1 className={styles.title}>Intel</h1>
<span className={styles.subtitle}> <span className={styles.subtitle}>THREAT INTELLIGENCE PRODUCTS</span>
THREAT INTELLIGENCE PRODUCTS
</span>
</div> </div>
<div className={styles.exports}> <div className={styles.exports}>
<span className={styles.exportLabel}>EXPORT</span> <span className={styles.exportLabel}>EXPORT</span>
<button <button type="button" className={styles.exportBtn} onClick={exportSTIX}>
type="button"
className={styles.exportBtn}
onClick={exportSTIX}
>
STIX 2.1 STIX 2.1
</button> </button>
{BLOCKLIST_FORMATS.map((fmt) => ( {BLOCKLIST_FORMATS.map((fmt) => (
@ -103,12 +89,8 @@ export function IntelPage() {
<td>{ioc.confidence}%</td> <td>{ioc.confidence}%</td>
<td>{ioc.sight_count}</td> <td>{ioc.sight_count}</td>
<td>{ioc.source}</td> <td>{ioc.source}</td>
<td> <td>{new Date(ioc.first_seen).toLocaleDateString()}</td>
{new Date(ioc.first_seen).toLocaleDateString()} <td>{new Date(ioc.last_seen).toLocaleDateString()}</td>
</td>
<td>
{new Date(ioc.last_seen).toLocaleDateString()}
</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
@ -120,25 +102,20 @@ export function IntelPage() {
className={styles.pageBtn} className={styles.pageBtn}
disabled={offset === 0} disabled={offset === 0}
onClick={() => onClick={() =>
setOffset( setOffset(Math.max(0, offset - PAGINATION.DEFAULT_LIMIT))
Math.max(0, offset - PAGINATION.DEFAULT_LIMIT)
)
} }
> >
&#9664; PREV &#9664; PREV
</button> </button>
<span className={styles.pageInfo}> <span className={styles.pageInfo}>
{offset + 1}&ndash; {offset + 1}&ndash;
{Math.min(offset + PAGINATION.DEFAULT_LIMIT, total)} OF{' '} {Math.min(offset + PAGINATION.DEFAULT_LIMIT, total)} OF {total}
{total}
</span> </span>
<button <button
type="button" type="button"
className={styles.pageBtn} className={styles.pageBtn}
disabled={offset + PAGINATION.DEFAULT_LIMIT >= total} disabled={offset + PAGINATION.DEFAULT_LIMIT >= total}
onClick={() => onClick={() => setOffset(offset + PAGINATION.DEFAULT_LIMIT)}
setOffset(offset + PAGINATION.DEFAULT_LIMIT)
}
> >
NEXT &#9654; NEXT &#9654;
</button> </button>

View File

@ -29,9 +29,7 @@ export function MitrePage() {
const { data: techniques } = useMitreTechniques() const { data: techniques } = useMitreTechniques()
const { data: heatmap } = useMitreHeatmap() const { data: heatmap } = useMitreHeatmap()
const countMap = new Map( const countMap = new Map(heatmap?.map((h) => [h.technique_id, h.count]))
heatmap?.map((h) => [h.technique_id, h.count])
)
const byTactic = new Map<string, typeof techniques>() const byTactic = new Map<string, typeof techniques>()
for (const t of techniques ?? []) { for (const t of techniques ?? []) {
@ -51,9 +49,7 @@ export function MitrePage() {
<span className={styles.legendLabel}>INTENSITY</span> <span className={styles.legendLabel}>INTENSITY</span>
{HEAT_THRESHOLDS.map((t, i) => ( {HEAT_THRESHOLDS.map((t, i) => (
<span key={t} className={styles.legendItem}> <span key={t} className={styles.legendItem}>
<span <span className={`${styles.legendSwatch} ${styles[`heat${i}`]}`} />
className={`${styles.legendSwatch} ${styles[`heat${i}`]}`}
/>
<span className={styles.legendText}>{t}+</span> <span className={styles.legendText}>{t}+</span>
</span> </span>
))} ))}
@ -66,9 +62,7 @@ export function MitrePage() {
return ( return (
<div key={tactic} className={styles.tacticColumn}> <div key={tactic} className={styles.tacticColumn}>
<h3 className={styles.tacticLabel}> <h3 className={styles.tacticLabel}>{tactic.replace(/-/g, ' ')}</h3>
{tactic.replace(/-/g, ' ')}
</h3>
<div className={styles.techniques}> <div className={styles.techniques}>
{techs.map((t) => { {techs.map((t) => {
const count = countMap.get(t.id) ?? 0 const count = countMap.get(t.id) ?? 0
@ -83,9 +77,7 @@ export function MitrePage() {
<span className={styles.techId}>{t.id}</span> <span className={styles.techId}>{t.id}</span>
<span className={styles.techName}>{t.name}</span> <span className={styles.techName}>{t.name}</span>
{count > 0 && ( {count > 0 && (
<span className={styles.techCount}> <span className={styles.techCount}>{count}</span>
{count}
</span>
)} )}
</div> </div>
) )

View File

@ -44,9 +44,7 @@ export function SessionDetailPage() {
&#8592; SESSIONS &#8592; SESSIONS
</Link> </Link>
<h1 className={styles.detailTitle}> <h1 className={styles.detailTitle}>SESSION {session.id.slice(0, 8)}</h1>
SESSION {session.id.slice(0, 8)}
</h1>
<div className={styles.dossier}> <div className={styles.dossier}>
{rows.map((row) => ( {rows.map((row) => (

View File

@ -52,10 +52,7 @@ export function SessionsPage() {
return ( return (
<tr key={s.id}> <tr key={s.id}>
<td> <td>
<Link <Link to={`/sessions/${s.id}`} className={styles.link}>
to={`/sessions/${s.id}`}
className={styles.link}
>
{s.id.slice(0, 8)} {s.id.slice(0, 8)}
</Link> </Link>
</td> </td>
@ -67,9 +64,7 @@ export function SessionsPage() {
<td>{s.command_count}</td> <td>{s.command_count}</td>
<td className={styles.threat}>{s.threat_score}</td> <td className={styles.threat}>{s.threat_score}</td>
<td>{new Date(s.started_at).toLocaleString()}</td> <td>{new Date(s.started_at).toLocaleString()}</td>
<td> <td>{duration !== null ? `${duration}s` : 'active'}</td>
{duration !== null ? `${duration}s` : 'active'}
</td>
</tr> </tr>
) )
})} })}
@ -81,15 +76,12 @@ export function SessionsPage() {
type="button" type="button"
className={styles.pageBtn} className={styles.pageBtn}
disabled={offset === 0} disabled={offset === 0}
onClick={() => onClick={() => setOffset(Math.max(0, offset - PAGE_SIZE))}
setOffset(Math.max(0, offset - PAGE_SIZE))
}
> >
&#9664; PREV &#9664; PREV
</button> </button>
<span className={styles.pageInfo}> <span className={styles.pageInfo}>
{offset + 1}&ndash;{Math.min(offset + PAGE_SIZE, total)}{' '} {offset + 1}&ndash;{Math.min(offset + PAGE_SIZE, total)} OF {total}
OF {total}
</span> </span>
<button <button
type="button" type="button"

View File

@ -9,8 +9,8 @@ import {
type AlertRule, type AlertRule,
useAlertRules, useAlertRules,
useChangePassword, useChangePassword,
useLogout,
useDeleteAlertRule, useDeleteAlertRule,
useLogout,
useUpdateAlertRule, useUpdateAlertRule,
useUpdateEmail, useUpdateEmail,
useUpdateProfile, useUpdateProfile,

View File

@ -15,13 +15,18 @@ const (
) )
func main() { func main() {
os.Exit(run())
}
func run() int {
client := &http.Client{Timeout: httpDialTO} client := &http.Client{Timeout: httpDialTO}
resp, err := client.Get(healthURL) resp, err := client.Get(healthURL)
if err != nil { if err != nil {
os.Exit(1) return 1
} }
defer func() { _ = resp.Body.Close() }() defer func() { _ = resp.Body.Close() }() //nolint:errcheck // healthcheck binary; close error not actionable
if resp.StatusCode < 200 || resp.StatusCode >= 300 { if resp.StatusCode < 200 || resp.StatusCode >= 300 {
os.Exit(1) return 1
} }
return 0
} }

View File

@ -29,11 +29,6 @@ Connects to:
attack/RuleAttack.hpp - RuleAttack for mutation mode attack/RuleAttack.hpp - RuleAttack for mutation mode
*/ */
#include <boost/program_options.hpp>
#include <expected>
#include <iostream>
#include <print>
#include <string>
#include "src/attack/BruteForceAttack.hpp" #include "src/attack/BruteForceAttack.hpp"
#include "src/attack/DictionaryAttack.hpp" #include "src/attack/DictionaryAttack.hpp"
#include "src/attack/RuleAttack.hpp" #include "src/attack/RuleAttack.hpp"
@ -45,20 +40,31 @@ Connects to:
#include "src/hash/SHA1Hasher.hpp" #include "src/hash/SHA1Hasher.hpp"
#include "src/hash/SHA256Hasher.hpp" #include "src/hash/SHA256Hasher.hpp"
#include "src/hash/SHA512Hasher.hpp" #include "src/hash/SHA512Hasher.hpp"
#include <boost/program_options.hpp>
#include <expected>
#include <iostream>
#include <print>
#include <string>
namespace po = boost::program_options; namespace po = boost::program_options;
static std::string build_charset(const std::string& spec) { static std::string build_charset(const std::string &spec) {
std::string result; std::string result;
auto has = [&](std::string_view token) { auto has = [&](std::string_view token) { return spec.find(token) != std::string::npos; };
return spec.find(token) != std::string::npos;
};
if (has("lower")) { result += config::CHARSET_LOWER; } if (has("lower")) {
if (has("upper")) { result += config::CHARSET_UPPER; } result += config::CHARSET_LOWER;
if (has("digits")) { result += config::CHARSET_DIGITS; } }
if (has("special")) { result += config::CHARSET_SPECIAL; } if (has("upper")) {
result += config::CHARSET_UPPER;
}
if (has("digits")) {
result += config::CHARSET_DIGITS;
}
if (has("special")) {
result += config::CHARSET_SPECIAL;
}
if (result.empty()) { if (result.empty()) {
result += config::CHARSET_LOWER; result += config::CHARSET_LOWER;
@ -69,8 +75,7 @@ static std::string build_charset(const std::string& spec) {
} }
template <Hasher H> template <Hasher H>
static auto dispatch_attack(const CrackConfig& cfg) static auto dispatch_attack(const CrackConfig &cfg) -> std::expected<CrackResult, CrackError> {
-> std::expected<CrackResult, CrackError> {
if (cfg.bruteforce) { if (cfg.bruteforce) {
return Engine::crack<H, BruteForceAttack>(cfg); return Engine::crack<H, BruteForceAttack>(cfg);
} }
@ -80,13 +85,17 @@ static auto dispatch_attack(const CrackConfig& cfg)
return Engine::crack<H, DictionaryAttack>(cfg); return Engine::crack<H, DictionaryAttack>(cfg);
} }
static auto dispatch_hasher(HashType type, const CrackConfig& cfg) static auto dispatch_hasher(HashType type, const CrackConfig &cfg)
-> std::expected<CrackResult, CrackError> { -> std::expected<CrackResult, CrackError> {
switch (type) { switch (type) {
case HashType::MD5: return dispatch_attack<MD5Hasher>(cfg); case HashType::MD5:
case HashType::SHA1: return dispatch_attack<SHA1Hasher>(cfg); return dispatch_attack<MD5Hasher>(cfg);
case HashType::SHA256: return dispatch_attack<SHA256Hasher>(cfg); case HashType::SHA1:
case HashType::SHA512: return dispatch_attack<SHA512Hasher>(cfg); return dispatch_attack<SHA1Hasher>(cfg);
case HashType::SHA256:
return dispatch_attack<SHA256Hasher>(cfg);
case HashType::SHA512:
return dispatch_attack<SHA512Hasher>(cfg);
} }
return std::unexpected(CrackError::UnsupportedAlgorithm); return std::unexpected(CrackError::UnsupportedAlgorithm);
} }
@ -96,68 +105,77 @@ static std::string json_escape(std::string_view s) {
result.reserve(s.size()); result.reserve(s.size());
for (char c : s) { for (char c : s) {
switch (c) { switch (c) {
case '"': result += "\\\""; break; case '"':
case '\\': result += "\\\\"; break; result += "\\\"";
case '\n': result += "\\n"; break; break;
case '\r': result += "\\r"; break; case '\\':
case '\t': result += "\\t"; break; result += "\\\\";
default: result += c; break; break;
case '\n':
result += "\\n";
break;
case '\r':
result += "\\r";
break;
case '\t':
result += "\\t";
break;
default:
result += c;
break;
} }
} }
return result; return result;
} }
static void write_json_result(const std::string& path, static void write_json_result(const std::string &path, const CrackResult &result) {
const CrackResult& result) { auto *f = std::fopen(path.c_str(), "w");
auto* f = std::fopen(path.c_str(), "w"); if (!f) {
if (!f) { return; } return;
}
std::fprintf(f, std::fprintf(f,
"{\n" "{\n"
" \"plaintext\": \"%s\",\n" " \"plaintext\": \"%s\",\n"
" \"hash\": \"%s\",\n" " \"hash\": \"%s\",\n"
" \"algorithm\": \"%s\",\n" " \"algorithm\": \"%s\",\n"
" \"elapsed_seconds\": %.4f,\n" " \"elapsed_seconds\": %.4f,\n"
" \"candidates_tested\": %zu,\n" " \"candidates_tested\": %zu,\n"
" \"hashes_per_second\": %.2f\n" " \"hashes_per_second\": %.2f\n"
"}\n", "}\n",
json_escape(result.plaintext).c_str(), json_escape(result.plaintext).c_str(), json_escape(result.hash).c_str(),
json_escape(result.hash).c_str(), json_escape(result.algorithm).c_str(), result.elapsed_seconds,
json_escape(result.algorithm).c_str(), result.candidates_tested, result.hashes_per_second);
result.elapsed_seconds,
result.candidates_tested,
result.hashes_per_second);
std::fclose(f); std::fclose(f);
} }
int main(int argc, char* argv[]) { int main(int argc, char *argv[]) {
po::options_description desc("hashcracker - Multi-threaded hash cracking tool"); po::options_description desc("hashcracker - Multi-threaded hash cracking tool");
desc.add_options() desc.add_options()("help,h", "Show help message")("hash", po::value<std::string>(),
("help,h", "Show help message") "Target hash to crack")(
("hash", po::value<std::string>(), "Target hash to crack") "type", po::value<std::string>()->default_value("auto"),
("type", po::value<std::string>()->default_value("auto"), "Hash type: md5, sha1, sha256, sha512, auto")("wordlist,w", po::value<std::string>(),
"Hash type: md5, sha1, sha256, sha512, auto") "Path to wordlist file")(
("wordlist,w", po::value<std::string>(), "Path to wordlist file") "bruteforce,b", "Use brute-force attack mode")(
("bruteforce,b", "Use brute-force attack mode") "charset", po::value<std::string>()->default_value("lower,digits"),
("charset", po::value<std::string>()->default_value("lower,digits"), "Character sets: lower,upper,digits,special")(
"Character sets: lower,upper,digits,special") "max-length", po::value<std::size_t>()->default_value(config::DEFAULT_MAX_BRUTE_LENGTH),
("max-length", po::value<std::size_t>()->default_value( "Max password length for brute-force")("rules,r",
config::DEFAULT_MAX_BRUTE_LENGTH), "Max password length for brute-force") "Apply mutation rules to dictionary words")(
("rules,r", "Apply mutation rules to dictionary words") "chain-rules", "Chain mutation rules in combination")("salt", po::value<std::string>(),
("chain-rules", "Chain mutation rules in combination") "Salt value to prepend/append")(
("salt", po::value<std::string>(), "Salt value to prepend/append") "salt-position", po::value<std::string>()->default_value("prepend"),
("salt-position", po::value<std::string>()->default_value("prepend"), "Salt position: prepend or append")(
"Salt position: prepend or append") "threads,t", po::value<unsigned>()->default_value(config::DEFAULT_THREAD_COUNT),
("threads,t", po::value<unsigned>()->default_value( "Thread count (0 = auto)")("output,o", po::value<std::string>(),
config::DEFAULT_THREAD_COUNT), "Thread count (0 = auto)") "Write JSON result to file");
("output,o", po::value<std::string>(), "Write JSON result to file");
po::variables_map vm; po::variables_map vm;
try { try {
po::store(po::parse_command_line(argc, argv, desc), vm); po::store(po::parse_command_line(argc, argv, desc), vm);
po::notify(vm); po::notify(vm);
} catch (const po::error& e) { } catch (const po::error &e) {
std::println(stderr, "Error: {}", e.what()); std::println(stderr, "Error: {}", e.what());
return 1; return 1;
} }
@ -198,8 +216,7 @@ int main(int argc, char* argv[]) {
if (cfg.hash_type == "auto") { if (cfg.hash_type == "auto") {
auto detected = HashDetector::detect(cfg.target_hash); auto detected = HashDetector::detect(cfg.target_hash);
if (!detected.has_value()) { if (!detected.has_value()) {
std::println(stderr, "Error: {}", std::println(stderr, "Error: {}", crack_error_message(detected.error()));
crack_error_message(detected.error()));
return 1; return 1;
} }
hash_type = *detected; hash_type = *detected;

View File

@ -26,8 +26,7 @@ Connects to:
#include "src/attack/BruteForceAttack.hpp" #include "src/attack/BruteForceAttack.hpp"
#include <algorithm> #include <algorithm>
std::size_t BruteForceAttack::compute_keyspace(std::size_t charset_size, std::size_t BruteForceAttack::compute_keyspace(std::size_t charset_size, std::size_t max_length) {
std::size_t max_length) {
std::size_t total = 0; std::size_t total = 0;
std::size_t power = 1; std::size_t power = 1;
for (std::size_t len = 1; len <= max_length; ++len) { for (std::size_t len = 1; len <= max_length; ++len) {
@ -37,17 +36,15 @@ std::size_t BruteForceAttack::compute_keyspace(std::size_t charset_size,
return total; return total;
} }
BruteForceAttack::BruteForceAttack(std::string_view charset, BruteForceAttack::BruteForceAttack(std::string_view charset, std::size_t max_length,
std::size_t max_length, unsigned thread_index, unsigned total_threads)
unsigned thread_index,
unsigned total_threads)
: charset_(charset), max_length_(max_length), : charset_(charset), max_length_(max_length),
total_keyspace_(compute_keyspace(charset.size(), max_length)) { total_keyspace_(compute_keyspace(charset.size(), max_length)) {
std::size_t per_thread = total_keyspace_ / total_threads; std::size_t per_thread = total_keyspace_ / total_threads;
std::size_t remainder = total_keyspace_ % total_threads; std::size_t remainder = total_keyspace_ % total_threads;
start_index_ = thread_index * per_thread start_index_ =
+ std::min(static_cast<std::size_t>(thread_index), remainder); thread_index * per_thread + std::min(static_cast<std::size_t>(thread_index), remainder);
std::size_t my_count = per_thread + (thread_index < remainder ? 1 : 0); std::size_t my_count = per_thread + (thread_index < remainder ? 1 : 0);
end_index_ = start_index_ + my_count; end_index_ = start_index_ + my_count;
current_index_ = start_index_; current_index_ = start_index_;
@ -83,5 +80,9 @@ std::expected<std::string, AttackComplete> BruteForceAttack::next() {
return index_to_candidate(current_index_++); return index_to_candidate(current_index_++);
} }
std::size_t BruteForceAttack::total() const { return total_keyspace_; } std::size_t BruteForceAttack::total() const {
std::size_t BruteForceAttack::progress() const { return current_index_ - start_index_; } return total_keyspace_;
}
std::size_t BruteForceAttack::progress() const {
return current_index_ - start_index_;
}

View File

@ -12,22 +12,22 @@ Connects to:
#pragma once #pragma once
#include "src/core/Concepts.hpp"
#include <cstddef> #include <cstddef>
#include <expected> #include <expected>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include "src/core/Concepts.hpp"
class BruteForceAttack { class BruteForceAttack {
public: public:
BruteForceAttack(std::string_view charset, std::size_t max_length, BruteForceAttack(std::string_view charset, std::size_t max_length, unsigned thread_index,
unsigned thread_index, unsigned total_threads); unsigned total_threads);
std::expected<std::string, AttackComplete> next(); std::expected<std::string, AttackComplete> next();
std::size_t total() const; std::size_t total() const;
std::size_t progress() const; std::size_t progress() const;
private: private:
std::string charset_; std::string charset_;
std::size_t max_length_; std::size_t max_length_;
std::size_t total_keyspace_; std::size_t total_keyspace_;
@ -36,6 +36,5 @@ private:
std::size_t current_index_; std::size_t current_index_;
std::string index_to_candidate(std::size_t index) const; std::string index_to_candidate(std::size_t index) const;
static std::size_t compute_keyspace(std::size_t charset_size, static std::size_t compute_keyspace(std::size_t charset_size, std::size_t max_length);
std::size_t max_length);
}; };

View File

@ -27,9 +27,7 @@ Connects to:
#include "src/attack/DictionaryAttack.hpp" #include "src/attack/DictionaryAttack.hpp"
#include <algorithm> #include <algorithm>
static std::size_t count_lines_in_range(const char* data, static std::size_t count_lines_in_range(const char *data, std::size_t start, std::size_t end) {
std::size_t start,
std::size_t end) {
std::size_t count = 0; std::size_t count = 0;
for (std::size_t i = start; i < end; ++i) { for (std::size_t i = start; i < end; ++i) {
if (data[i] == '\n') { if (data[i] == '\n') {
@ -39,23 +37,21 @@ static std::size_t count_lines_in_range(const char* data,
return count; return count;
} }
static std::size_t find_next_newline(const char* data, static std::size_t find_next_newline(const char *data, std::size_t pos, std::size_t size) {
std::size_t pos,
std::size_t size) {
while (pos < size && data[pos] != '\n') { while (pos < size && data[pos] != '\n') {
++pos; ++pos;
} }
return pos < size ? pos + 1 : size; return pos < size ? pos + 1 : size;
} }
std::expected<DictionaryAttack, CrackError> DictionaryAttack::create( std::expected<DictionaryAttack, CrackError>
std::string_view path, unsigned thread_index, unsigned total_threads) { DictionaryAttack::create(std::string_view path, unsigned thread_index, unsigned total_threads) {
auto file = MappedFile::open(path); auto file = MappedFile::open(path);
if (!file.has_value()) { if (!file.has_value()) {
return std::unexpected(file.error()); return std::unexpected(file.error());
} }
auto* data = file->data(); auto *data = file->data();
auto file_size = file->size(); auto file_size = file->size();
std::size_t total_lines = count_lines_in_range(data, 0, file_size); std::size_t total_lines = count_lines_in_range(data, 0, file_size);
@ -66,10 +62,9 @@ std::expected<DictionaryAttack, CrackError> DictionaryAttack::create(
std::size_t lines_per_thread = total_lines / total_threads; std::size_t lines_per_thread = total_lines / total_threads;
std::size_t remainder = total_lines % total_threads; std::size_t remainder = total_lines % total_threads;
std::size_t my_start_line = thread_index * lines_per_thread std::size_t my_start_line = thread_index * lines_per_thread +
+ std::min(static_cast<std::size_t>(thread_index), remainder); std::min(static_cast<std::size_t>(thread_index), remainder);
std::size_t my_line_count = lines_per_thread std::size_t my_line_count = lines_per_thread + (thread_index < remainder ? 1 : 0);
+ (thread_index < remainder ? 1 : 0);
std::size_t start_offset = 0; std::size_t start_offset = 0;
for (std::size_t i = 0; i < my_start_line; ++i) { for (std::size_t i = 0; i < my_start_line; ++i) {
@ -116,5 +111,9 @@ std::expected<std::string, AttackComplete> DictionaryAttack::next() {
return std::unexpected(AttackComplete{}); return std::unexpected(AttackComplete{});
} }
std::size_t DictionaryAttack::total() const { return total_words_; } std::size_t DictionaryAttack::total() const {
std::size_t DictionaryAttack::progress() const { return words_read_; } return total_words_;
}
std::size_t DictionaryAttack::progress() const {
return words_read_;
}

View File

@ -14,23 +14,23 @@ Connects to:
#pragma once #pragma once
#include "src/core/Concepts.hpp"
#include "src/io/MappedFile.hpp"
#include <cstddef> #include <cstddef>
#include <expected> #include <expected>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include "src/core/Concepts.hpp"
#include "src/io/MappedFile.hpp"
class DictionaryAttack { class DictionaryAttack {
public: public:
static std::expected<DictionaryAttack, CrackError> create( static std::expected<DictionaryAttack, CrackError>
std::string_view path, unsigned thread_index, unsigned total_threads); create(std::string_view path, unsigned thread_index, unsigned total_threads);
std::expected<std::string, AttackComplete> next(); std::expected<std::string, AttackComplete> next();
std::size_t total() const; std::size_t total() const;
std::size_t progress() const; std::size_t progress() const;
private: private:
DictionaryAttack() = default; DictionaryAttack() = default;
MappedFile file_; MappedFile file_;

View File

@ -30,9 +30,9 @@ Connects to:
RuleAttack::RuleAttack(DictionaryAttack dict, bool chain_rules) RuleAttack::RuleAttack(DictionaryAttack dict, bool chain_rules)
: dict_(std::move(dict)), chain_rules_(chain_rules) {} : dict_(std::move(dict)), chain_rules_(chain_rules) {}
std::expected<RuleAttack, CrackError> RuleAttack::create( std::expected<RuleAttack, CrackError> RuleAttack::create(std::string_view path, bool chain_rules,
std::string_view path, bool chain_rules, unsigned thread_index,
unsigned thread_index, unsigned total_threads) { unsigned total_threads) {
auto dict = DictionaryAttack::create(path, thread_index, total_threads); auto dict = DictionaryAttack::create(path, thread_index, total_threads);
if (!dict.has_value()) { if (!dict.has_value()) {
return std::unexpected(dict.error()); return std::unexpected(dict.error());
@ -49,14 +49,14 @@ bool RuleAttack::load_next_word() {
mutations_.clear(); mutations_.clear();
mutations_.push_back(*word); mutations_.push_back(*word);
for (auto&& m : RuleSet::apply_all(*word)) { for (auto &&m : RuleSet::apply_all(*word)) {
mutations_.push_back(std::move(m)); mutations_.push_back(std::move(m));
} }
if (chain_rules_) { if (chain_rules_) {
std::vector<std::string> base(mutations_.begin() + 1, mutations_.end()); std::vector<std::string> base(mutations_.begin() + 1, mutations_.end());
for (const auto& b : base) { for (const auto &b : base) {
for (auto&& m : RuleSet::apply_all(b)) { for (auto &&m : RuleSet::apply_all(b)) {
mutations_.push_back(std::move(m)); mutations_.push_back(std::move(m));
} }
} }
@ -77,5 +77,9 @@ std::expected<std::string, AttackComplete> RuleAttack::next() {
return std::move(mutations_[mutation_index_++]); return std::move(mutations_[mutation_index_++]);
} }
std::size_t RuleAttack::total() const { return dict_.total(); } std::size_t RuleAttack::total() const {
std::size_t RuleAttack::progress() const { return candidates_yielded_; } return dict_.total();
}
std::size_t RuleAttack::progress() const {
return candidates_yielded_;
}

View File

@ -17,25 +17,24 @@ Connects to:
#pragma once #pragma once
#include "src/attack/DictionaryAttack.hpp"
#include "src/core/Concepts.hpp"
#include <cstddef> #include <cstddef>
#include <expected> #include <expected>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <vector> #include <vector>
#include "src/attack/DictionaryAttack.hpp"
#include "src/core/Concepts.hpp"
class RuleAttack { class RuleAttack {
public: public:
static std::expected<RuleAttack, CrackError> create( static std::expected<RuleAttack, CrackError>
std::string_view path, bool chain_rules, create(std::string_view path, bool chain_rules, unsigned thread_index, unsigned total_threads);
unsigned thread_index, unsigned total_threads);
std::expected<std::string, AttackComplete> next(); std::expected<std::string, AttackComplete> next();
std::size_t total() const; std::size_t total() const;
std::size_t progress() const; std::size_t progress() const;
private: private:
RuleAttack(DictionaryAttack dict, bool chain_rules); RuleAttack(DictionaryAttack dict, bool chain_rules);
DictionaryAttack dict_; DictionaryAttack dict_;

View File

@ -70,7 +70,7 @@ constexpr std::string_view CYAN = "\033[36m";
constexpr std::string_view BOLD = "\033[1m"; constexpr std::string_view BOLD = "\033[1m";
constexpr std::string_view DIM = "\033[2m"; constexpr std::string_view DIM = "\033[2m";
} } // namespace color
namespace box { namespace box {
@ -85,7 +85,7 @@ constexpr std::string_view BLOCK_EMPTY = "\u2591";
constexpr std::string_view BAR_LEFT = "\u2590"; constexpr std::string_view BAR_LEFT = "\u2590";
constexpr std::string_view BAR_RIGHT = "\u258C"; constexpr std::string_view BAR_RIGHT = "\u258C";
} } // namespace box
namespace symbol { namespace symbol {
@ -99,9 +99,9 @@ constexpr std::string_view TRIANGLE_UP = "\u25B2";
constexpr std::string_view STAR = "\u2726"; constexpr std::string_view STAR = "\u2726";
constexpr std::string_view DIVIDER_CHAR = "\u2501"; constexpr std::string_view DIVIDER_CHAR = "\u2501";
} } // namespace symbol
} } // namespace config
struct CrackConfig { struct CrackConfig {
std::string target_hash; std::string target_hash;

View File

@ -50,12 +50,18 @@ enum class CrackError {
constexpr std::string_view crack_error_message(CrackError e) { constexpr std::string_view crack_error_message(CrackError e) {
switch (e) { switch (e) {
case CrackError::FileNotFound: return "File not found"; case CrackError::FileNotFound:
case CrackError::InvalidHash: return "Invalid hash format"; return "File not found";
case CrackError::UnsupportedAlgorithm: return "Unsupported hash algorithm"; case CrackError::InvalidHash:
case CrackError::OpenSSLError: return "OpenSSL internal error"; return "Invalid hash format";
case CrackError::InvalidConfig: return "Invalid configuration"; case CrackError::UnsupportedAlgorithm:
case CrackError::Exhausted: return "All candidates exhausted"; return "Unsupported hash algorithm";
case CrackError::OpenSSLError:
return "OpenSSL internal error";
case CrackError::InvalidConfig:
return "Invalid configuration";
case CrackError::Exhausted:
return "All candidates exhausted";
} }
return "Unknown error"; return "Unknown error";
} }

View File

@ -31,11 +31,6 @@ Connects to:
#pragma once #pragma once
#include <chrono>
#include <cstdio>
#include <expected>
#include <string>
#include <thread>
#include "src/attack/BruteForceAttack.hpp" #include "src/attack/BruteForceAttack.hpp"
#include "src/attack/DictionaryAttack.hpp" #include "src/attack/DictionaryAttack.hpp"
#include "src/attack/RuleAttack.hpp" #include "src/attack/RuleAttack.hpp"
@ -43,26 +38,32 @@ Connects to:
#include "src/core/Concepts.hpp" #include "src/core/Concepts.hpp"
#include "src/display/Progress.hpp" #include "src/display/Progress.hpp"
#include "src/threading/ThreadPool.hpp" #include "src/threading/ThreadPool.hpp"
#include <chrono>
#include <cstdio>
#include <expected>
#include <string>
#include <thread>
class Engine { class Engine {
public: public:
template <Hasher H, AttackStrategy A> template <Hasher H, AttackStrategy A>
static auto crack(const CrackConfig& cfg) static auto crack(const CrackConfig &cfg) -> std::expected<CrackResult, CrackError>;
-> std::expected<CrackResult, CrackError>;
}; };
template <Hasher H, AttackStrategy A> template <Hasher H, AttackStrategy A>
auto Engine::crack(const CrackConfig& cfg) auto Engine::crack(const CrackConfig &cfg) -> std::expected<CrackResult, CrackError> {
-> std::expected<CrackResult, CrackError> { unsigned thread_count =
unsigned thread_count = cfg.thread_count > 0 cfg.thread_count > 0 ? cfg.thread_count : std::thread::hardware_concurrency();
? cfg.thread_count
: std::thread::hardware_concurrency();
ThreadPool pool(thread_count); ThreadPool pool(thread_count);
auto attack_name = [&]() -> std::string_view { auto attack_name = [&]() -> std::string_view {
if (cfg.bruteforce) { return "Brute Force"; } if (cfg.bruteforce) {
if (cfg.use_rules) { return "Rules"; } return "Brute Force";
}
if (cfg.use_rules) {
return "Rules";
}
return "Dictionary"; return "Dictionary";
}(); }();
@ -72,17 +73,20 @@ auto Engine::crack(const CrackConfig& cfg)
return probe.total(); return probe.total();
} else if constexpr (std::same_as<A, RuleAttack>) { } else if constexpr (std::same_as<A, RuleAttack>) {
auto probe = DictionaryAttack::create(cfg.wordlist_path, 0, 1); auto probe = DictionaryAttack::create(cfg.wordlist_path, 0, 1);
if (!probe) { return 0; } if (!probe) {
return 0;
}
return probe->total() * 2005; return probe->total() * 2005;
} else { } else {
auto probe = DictionaryAttack::create(cfg.wordlist_path, 0, 1); auto probe = DictionaryAttack::create(cfg.wordlist_path, 0, 1);
if (!probe) { return 0; } if (!probe) {
return 0;
}
return probe->total(); return probe->total();
} }
}(); }();
Progress progress(H::name(), attack_name, thread_count, Progress progress(H::name(), attack_name, thread_count, total_estimate, pool.state().found,
total_estimate, pool.state().found,
pool.state().tested_count); pool.state().tested_count);
if (Progress::is_tty()) { if (Progress::is_tty()) {
@ -97,16 +101,14 @@ auto Engine::crack(const CrackConfig& cfg)
std::jthread display_thread; std::jthread display_thread;
if (Progress::is_tty()) { if (Progress::is_tty()) {
display_thread = std::jthread([&](std::stop_token st) { display_thread = std::jthread([&](std::stop_token st) {
while (!st.stop_requested() && while (!st.stop_requested() && !pool.state().found.load(std::memory_order_relaxed)) {
!pool.state().found.load(std::memory_order_relaxed)) {
progress.update(); progress.update();
std::this_thread::sleep_for( std::this_thread::sleep_for(std::chrono::milliseconds(config::PROGRESS_UPDATE_MS));
std::chrono::milliseconds(config::PROGRESS_UPDATE_MS));
} }
}); });
} }
pool.run([&](unsigned tid, unsigned total, SharedState& state) { pool.run([&](unsigned tid, unsigned total, SharedState &state) {
H hasher; H hasher;
auto create_attack = [&]() { auto create_attack = [&]() {
@ -114,20 +116,23 @@ auto Engine::crack(const CrackConfig& cfg)
return std::expected<BruteForceAttack, CrackError>( return std::expected<BruteForceAttack, CrackError>(
BruteForceAttack(cfg.charset, cfg.max_length, tid, total)); BruteForceAttack(cfg.charset, cfg.max_length, tid, total));
} else if constexpr (std::same_as<A, RuleAttack>) { } else if constexpr (std::same_as<A, RuleAttack>) {
return RuleAttack::create( return RuleAttack::create(cfg.wordlist_path, cfg.chain_rules, tid, total);
cfg.wordlist_path, cfg.chain_rules, tid, total);
} else { } else {
return DictionaryAttack::create(cfg.wordlist_path, tid, total); return DictionaryAttack::create(cfg.wordlist_path, tid, total);
} }
}; };
auto attack = create_attack(); auto attack = create_attack();
if (!attack.has_value()) { return; } if (!attack.has_value()) {
return;
}
std::size_t local_count = 0; std::size_t local_count = 0;
while (!state.found.load(std::memory_order_relaxed)) { while (!state.found.load(std::memory_order_relaxed)) {
auto candidate = attack->next(); auto candidate = attack->next();
if (!candidate.has_value()) { break; } if (!candidate.has_value()) {
break;
}
std::string to_hash = *candidate; std::string to_hash = *candidate;
if (!cfg.salt.empty()) { if (!cfg.salt.empty()) {
@ -161,18 +166,16 @@ auto Engine::crack(const CrackConfig& cfg)
auto end = std::chrono::steady_clock::now(); auto end = std::chrono::steady_clock::now();
double elapsed = std::chrono::duration<double>(end - start).count(); double elapsed = std::chrono::duration<double>(end - start).count();
auto tested = pool.state().tested_count.load(std::memory_order_relaxed); auto tested = pool.state().tested_count.load(std::memory_order_relaxed);
double speed = (elapsed > 0.0) ? static_cast<double>(tested) / elapsed : 0.0;
auto& state = pool.state(); auto &state = pool.state();
if (state.found.load(std::memory_order_relaxed) && state.result.has_value()) { if (state.found.load(std::memory_order_relaxed) && state.result.has_value()) {
CrackResult result{ double speed = (elapsed > 0.0) ? static_cast<double>(tested) / elapsed : 0.0;
.plaintext = *state.result, CrackResult result{.plaintext = *state.result,
.hash = cfg.target_hash, .hash = cfg.target_hash,
.algorithm = std::string(H::name()), .algorithm = std::string(H::name()),
.elapsed_seconds = elapsed, .elapsed_seconds = elapsed,
.candidates_tested = tested, .candidates_tested = tested,
.hashes_per_second = speed .hashes_per_second = speed};
};
progress.print_cracked(result); progress.print_cracked(result);
return result; return result;

View File

@ -35,13 +35,11 @@ Connects to:
#include <sys/ioctl.h> #include <sys/ioctl.h>
#include <unistd.h> #include <unistd.h>
Progress::Progress(std::string_view algorithm, std::string_view attack_mode, Progress::Progress(std::string_view algorithm, std::string_view attack_mode, unsigned thread_count,
unsigned thread_count, std::size_t total_candidates, std::size_t total_candidates, const std::atomic<bool> &found,
const std::atomic<bool>& found, const std::atomic<std::size_t> &tested)
const std::atomic<std::size_t>& tested) : algorithm_(algorithm), attack_mode_(attack_mode), thread_count_(thread_count),
: algorithm_(algorithm), attack_mode_(attack_mode), total_(total_candidates), found_(found), tested_(tested),
thread_count_(thread_count), total_(total_candidates),
found_(found), tested_(tested),
start_time_(std::chrono::steady_clock::now()) {} start_time_(std::chrono::steady_clock::now()) {}
bool Progress::is_tty() { bool Progress::is_tty() {
@ -85,7 +83,9 @@ std::string Progress::render_bar(double fraction, std::size_t width) const {
} }
auto filled = static_cast<std::size_t>(fraction * static_cast<double>(width)); auto filled = static_cast<std::size_t>(fraction * static_cast<double>(width));
if (filled > width) { filled = width; } if (filled > width) {
filled = width;
}
std::string bar; std::string bar;
bar += config::box::BAR_LEFT; bar += config::box::BAR_LEFT;
@ -97,7 +97,9 @@ std::string Progress::render_bar(double fraction, std::size_t width) const {
} }
void Progress::print_banner() const { void Progress::print_banner() const {
if (!is_tty()) { return; } if (!is_tty()) {
return;
}
auto w = terminal_width(); auto w = terminal_width();
auto inner_width = (w > 6) ? w - 6 : 40; auto inner_width = (w > 6) ? w - 6 : 40;
@ -111,68 +113,58 @@ void Progress::print_banner() const {
bot_border += config::box::HORIZONTAL; bot_border += config::box::HORIZONTAL;
} }
auto line1 = std::format(" {} {} v{}", auto line1 =
config::APP_NAME, config::box::VERTICAL, config::VERSION); std::format(" {} {} v{}", config::APP_NAME, config::box::VERTICAL, config::VERSION);
auto line2 = std::format(" {} {} {} {} {} threads", auto line2 = std::format(" {} {} {} {} {} threads", config::box::VERTICAL, algorithm_,
config::box::VERTICAL, algorithm_, config::box::VERTICAL, config::box::VERTICAL, attack_mode_, thread_count_);
attack_mode_, thread_count_);
std::println("{}{}{}{}{}", std::println("{}{}{}{}{}", config::color::CYAN, config::box::TOP_LEFT, top_border,
config::color::CYAN, config::box::TOP_LEFT, config::box::TOP_RIGHT, config::color::RESET);
top_border, config::box::TOP_RIGHT, config::color::RESET); std::println("{}{} {:<{}}{}{}", config::color::CYAN, config::box::VERTICAL, line1,
std::println("{}{} {:<{}}{}{}", config::color::CYAN, inner_width - 1, config::box::VERTICAL, config::color::RESET);
config::box::VERTICAL, line1, inner_width - 1, std::println("{}{} {:<{}}{}{}", config::color::CYAN, config::box::VERTICAL, line2,
config::box::VERTICAL, config::color::RESET); inner_width - 1, config::box::VERTICAL, config::color::RESET);
std::println("{}{} {:<{}}{}{}", config::color::CYAN, std::println("{}{}{}{}{}", config::color::CYAN, config::box::BOTTOM_LEFT, bot_border,
config::box::VERTICAL, line2, inner_width - 1, config::box::BOTTOM_RIGHT, config::color::RESET);
config::box::VERTICAL, config::color::RESET);
std::println("{}{}{}{}{}",
config::color::CYAN, config::box::BOTTOM_LEFT,
bot_border, config::box::BOTTOM_RIGHT, config::color::RESET);
std::println(""); std::println("");
} }
void Progress::update() { void Progress::update() {
if (!is_tty()) { return; } if (!is_tty()) {
return;
}
auto now = std::chrono::steady_clock::now(); auto now = std::chrono::steady_clock::now();
double elapsed = std::chrono::duration<double>(now - start_time_).count(); double elapsed = std::chrono::duration<double>(now - start_time_).count();
auto tested_val = tested_.load(std::memory_order_relaxed); auto tested_val = tested_.load(std::memory_order_relaxed);
double fraction = (total_ > 0) double fraction =
? static_cast<double>(tested_val) / static_cast<double>(total_) (total_ > 0) ? static_cast<double>(tested_val) / static_cast<double>(total_) : 0.0;
: 0.0; if (fraction > 1.0) {
if (fraction > 1.0) { fraction = 1.0; } fraction = 1.0;
}
double speed = (elapsed > 0.0) double speed = (elapsed > 0.0) ? static_cast<double>(tested_val) / elapsed : 0.0;
? static_cast<double>(tested_val) / elapsed
: 0.0;
double eta = (speed > 0.0 && total_ > tested_val) double eta = (speed > 0.0 && total_ > tested_val)
? static_cast<double>(total_ - tested_val) / speed ? static_cast<double>(total_ - tested_val) / speed
: 0.0; : 0.0;
auto bar_width = terminal_width(); auto bar_width = terminal_width();
bar_width = (bar_width > 30) ? bar_width - 20 : 10; bar_width = (bar_width > 30) ? bar_width - 20 : 10;
std::print("\033[3A"); std::print("\033[3A");
std::println(" {}{} {:.1f}%{}", std::println(" {}{} {:.1f}%{}", config::color::YELLOW, render_bar(fraction, bar_width),
config::color::YELLOW, render_bar(fraction, bar_width), fraction * 100.0, config::color::RESET);
fraction * 100.0, config::color::RESET); std::println(" {} {} {} {} {} {} {} ~{}{}", config::symbol::DIAMOND, config::color::CYAN,
std::println(" {} {} {} {} {} {} {} ~{}{}", format_speed(speed), config::color::RESET, config::symbol::TIMER,
config::symbol::DIAMOND, config::color::CYAN, config::color::CYAN, format_time(elapsed), format_time(eta), config::color::RESET);
format_speed(speed), config::color::RESET, std::println(" {} {} {} / {} candidates{}", config::symbol::ARROW_RIGHT, config::color::CYAN,
config::symbol::TIMER, config::color::CYAN, format_count(tested_val), format_count(total_), config::color::RESET);
format_time(elapsed), format_time(eta),
config::color::RESET);
std::println(" {} {} {} / {} candidates{}",
config::symbol::ARROW_RIGHT, config::color::CYAN,
format_count(tested_val), format_count(total_),
config::color::RESET);
} }
void Progress::print_cracked(const CrackResult& result) const { void Progress::print_cracked(const CrackResult &result) const {
if (!is_tty()) { if (!is_tty()) {
std::println("{}", result.plaintext); std::println("{}", result.plaintext);
return; return;
@ -180,22 +172,17 @@ void Progress::print_cracked(const CrackResult& result) const {
std::print("\033[3A\033[J"); std::print("\033[3A\033[J");
std::println(" {}{} CRACKED {}{}{}", std::println(" {}{} CRACKED {}{}{}", config::color::GREEN, config::symbol::CHECK,
config::color::GREEN, config::symbol::CHECK, std::string(30, '-'), config::color::RESET, "");
std::string(30, '-'), config::color::RESET, ""); std::println(" {}Password: {}{}{}", config::color::BOLD, config::color::GREEN,
std::println(" {}Password: {}{}{}", result.plaintext, config::color::RESET);
config::color::BOLD, config::color::GREEN,
result.plaintext, config::color::RESET);
std::println(" Hash: {}", result.hash); std::println(" Hash: {}", result.hash);
std::println(" Algorithm: {}", result.algorithm); std::println(" Algorithm: {}", result.algorithm);
std::println(" Time: {} {} {}", std::println(" Time: {} {} {}", format_time(result.elapsed_seconds),
format_time(result.elapsed_seconds), config::box::VERTICAL, format_speed(result.hashes_per_second));
config::box::VERTICAL,
format_speed(result.hashes_per_second));
} }
void Progress::print_exhausted(std::string_view hash, void Progress::print_exhausted(std::string_view hash, std::string_view algorithm) const {
std::string_view algorithm) const {
if (!is_tty()) { if (!is_tty()) {
std::println("NOT FOUND"); std::println("NOT FOUND");
return; return;
@ -207,11 +194,10 @@ void Progress::print_exhausted(std::string_view hash,
std::print("\033[3A\033[J"); std::print("\033[3A\033[J");
std::println(" {}{} EXHAUSTED {}{}{}", std::println(" {}{} EXHAUSTED {}{}{}", config::color::RED, config::symbol::CROSS,
config::color::RED, config::symbol::CROSS, std::string(30, '-'), config::color::RESET, "");
std::string(30, '-'), config::color::RESET, "");
std::println(" Hash: {}", hash); std::println(" Hash: {}", hash);
std::println(" Algorithm: {}", algorithm); std::println(" Algorithm: {}", algorithm);
std::println(" Tested: {} candidates in {}", std::println(" Tested: {} candidates in {}", format_count(tested_val),
format_count(tested_val), format_time(elapsed)); format_time(elapsed));
} }

View File

@ -21,25 +21,24 @@ Connects to:
struct CrackResult; struct CrackResult;
class Progress { class Progress {
public: public:
Progress(std::string_view algorithm, std::string_view attack_mode, Progress(std::string_view algorithm, std::string_view attack_mode, unsigned thread_count,
unsigned thread_count, std::size_t total_candidates, std::size_t total_candidates, const std::atomic<bool> &found,
const std::atomic<bool>& found, const std::atomic<std::size_t> &tested);
const std::atomic<std::size_t>& tested);
void print_banner() const; void print_banner() const;
void update(); void update();
void print_cracked(const CrackResult& result) const; void print_cracked(const CrackResult &result) const;
void print_exhausted(std::string_view hash, std::string_view algorithm) const; void print_exhausted(std::string_view hash, std::string_view algorithm) const;
static bool is_tty(); static bool is_tty();
private: private:
std::string algorithm_; std::string algorithm_;
std::string attack_mode_; std::string attack_mode_;
unsigned thread_count_; unsigned thread_count_;
std::size_t total_; std::size_t total_;
const std::atomic<bool>& found_; const std::atomic<bool> &found_;
const std::atomic<std::size_t>& tested_; const std::atomic<std::size_t> &tested_;
std::chrono::steady_clock::time_point start_time_; std::chrono::steady_clock::time_point start_time_;

View File

@ -42,62 +42,59 @@ Connects to:
namespace evp { namespace evp {
struct MD5Tag { struct MD5Tag {
static const EVP_MD* algorithm() { return EVP_md5(); } static const EVP_MD *algorithm() { return EVP_md5(); }
static constexpr std::string_view name = "MD5"; static constexpr std::string_view name = "MD5";
static constexpr std::size_t hex_length = 32; static constexpr std::size_t hex_length = 32;
}; };
struct SHA1Tag { struct SHA1Tag {
static const EVP_MD* algorithm() { return EVP_sha1(); } static const EVP_MD *algorithm() { return EVP_sha1(); }
static constexpr std::string_view name = "SHA1"; static constexpr std::string_view name = "SHA1";
static constexpr std::size_t hex_length = 40; static constexpr std::size_t hex_length = 40;
}; };
struct SHA256Tag { struct SHA256Tag {
static const EVP_MD* algorithm() { return EVP_sha256(); } static const EVP_MD *algorithm() { return EVP_sha256(); }
static constexpr std::string_view name = "SHA256"; static constexpr std::string_view name = "SHA256";
static constexpr std::size_t hex_length = 64; static constexpr std::size_t hex_length = 64;
}; };
struct SHA512Tag { struct SHA512Tag {
static const EVP_MD* algorithm() { return EVP_sha512(); } static const EVP_MD *algorithm() { return EVP_sha512(); }
static constexpr std::string_view name = "SHA512"; static constexpr std::string_view name = "SHA512";
static constexpr std::size_t hex_length = 128; static constexpr std::size_t hex_length = 128;
}; };
} } // namespace evp
inline constexpr std::array<std::array<char, 2>, 256> HEX_TABLE = [] { inline constexpr std::array<std::array<char, 2>, 256> HEX_TABLE = [] {
std::array<std::array<char, 2>, 256> t{}; std::array<std::array<char, 2>, 256> t{};
constexpr std::array<char, 17> digits = { constexpr std::array<char, 17> digits = {'0', '1', '2', '3', '4', '5', '6', '7', '8',
'0','1','2','3','4','5','6','7', '9', 'a', 'b', 'c', 'd', 'e', 'f', '\0'};
'8','9','a','b','c','d','e','f','\0'};
for (std::size_t i = 0; i < 256; ++i) { for (std::size_t i = 0; i < 256; ++i) {
t.at(i) = {digits.at(i >> 4), digits.at(i & 0xF)}; t.at(i) = {digits.at(i >> 4), digits.at(i & 0xF)};
} }
return t; return t;
}(); }();
template <typename Tag> template <typename Tag> class EVPHasher {
class EVPHasher { public:
public:
std::string hash(std::string_view input) const { std::string hash(std::string_view input) const {
auto ctx = std::unique_ptr<EVP_MD_CTX, decltype(&EVP_MD_CTX_free)>( auto ctx = std::unique_ptr<EVP_MD_CTX, decltype(&EVP_MD_CTX_free)>(EVP_MD_CTX_new(),
EVP_MD_CTX_new(), EVP_MD_CTX_free); EVP_MD_CTX_free);
std::array<unsigned char, EVP_MAX_MD_SIZE> digest{}; std::array<unsigned char, EVP_MAX_MD_SIZE> digest{};
unsigned int len = 0; unsigned int len = 0;
if (!ctx if (!ctx || !EVP_DigestInit_ex(ctx.get(), Tag::algorithm(), nullptr) ||
|| !EVP_DigestInit_ex(ctx.get(), Tag::algorithm(), nullptr) !EVP_DigestUpdate(ctx.get(), input.data(), input.size()) ||
|| !EVP_DigestUpdate(ctx.get(), input.data(), input.size()) !EVP_DigestFinal_ex(ctx.get(), digest.data(), &len)) {
|| !EVP_DigestFinal_ex(ctx.get(), digest.data(), &len)) {
return ""; return "";
} }
std::string hex(static_cast<std::size_t>(len) * 2, '\0'); std::string hex(static_cast<std::size_t>(len) * 2, '\0');
for (std::size_t i = 0; i < len; ++i) { for (std::size_t i = 0; i < len; ++i) {
hex.at(i * 2) = HEX_TABLE.at(digest.at(i)).at(0); hex.at(i * 2) = HEX_TABLE.at(digest.at(i)).at(0);
hex.at(i * 2 + 1) = HEX_TABLE.at(digest.at(i)).at(1); hex.at(i * 2 + 1) = HEX_TABLE.at(digest.at(i)).at(1);
} }
return hex; return hex;

View File

@ -26,9 +26,7 @@ Connects to:
std::expected<HashType, CrackError> HashDetector::detect(std::string_view hash) { std::expected<HashType, CrackError> HashDetector::detect(std::string_view hash) {
auto is_hex = [](char c) { auto is_hex = [](char c) {
return (c >= '0' && c <= '9') || return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
(c >= 'a' && c <= 'f') ||
(c >= 'A' && c <= 'F');
}; };
if (!std::ranges::all_of(hash, is_hex)) { if (!std::ranges::all_of(hash, is_hex)) {
@ -36,10 +34,15 @@ std::expected<HashType, CrackError> HashDetector::detect(std::string_view hash)
} }
switch (hash.size()) { switch (hash.size()) {
case config::MD5_HEX_LENGTH: return HashType::MD5; case config::MD5_HEX_LENGTH:
case config::SHA1_HEX_LENGTH: return HashType::SHA1; return HashType::MD5;
case config::SHA256_HEX_LENGTH: return HashType::SHA256; case config::SHA1_HEX_LENGTH:
case config::SHA512_HEX_LENGTH: return HashType::SHA512; return HashType::SHA1;
default: return std::unexpected(CrackError::InvalidHash); case config::SHA256_HEX_LENGTH:
return HashType::SHA256;
case config::SHA512_HEX_LENGTH:
return HashType::SHA512;
default:
return std::unexpected(CrackError::InvalidHash);
} }
} }

View File

@ -12,13 +12,13 @@ Connects to:
#pragma once #pragma once
#include "src/core/Concepts.hpp"
#include <expected> #include <expected>
#include <string_view> #include <string_view>
#include "src/core/Concepts.hpp"
enum class HashType { MD5, SHA1, SHA256, SHA512 }; enum class HashType { MD5, SHA1, SHA256, SHA512 };
class HashDetector { class HashDetector {
public: public:
static std::expected<HashType, CrackError> detect(std::string_view hash); static std::expected<HashType, CrackError> detect(std::string_view hash);
}; };

View File

@ -47,15 +47,15 @@ std::expected<MappedFile, CrackError> MappedFile::open(std::string_view path) {
return std::unexpected(CrackError::InvalidConfig); return std::unexpected(CrackError::InvalidConfig);
} }
auto* mapped = static_cast<const char*>( auto *mapped =
mmap(nullptr, file_size, PROT_READ, MAP_PRIVATE, fd, 0)); static_cast<const char *>(mmap(nullptr, file_size, PROT_READ, MAP_PRIVATE, fd, 0));
if (mapped == MAP_FAILED) { if (mapped == MAP_FAILED) {
::close(fd); ::close(fd);
return std::unexpected(CrackError::FileNotFound); return std::unexpected(CrackError::FileNotFound);
} }
madvise(const_cast<char*>(mapped), file_size, MADV_SEQUENTIAL); madvise(const_cast<char *>(mapped), file_size, MADV_SEQUENTIAL);
MappedFile mf; MappedFile mf;
mf.data_ = mapped; mf.data_ = mapped;
@ -66,23 +66,23 @@ std::expected<MappedFile, CrackError> MappedFile::open(std::string_view path) {
MappedFile::~MappedFile() { MappedFile::~MappedFile() {
if (data_ && data_ != MAP_FAILED) { if (data_ && data_ != MAP_FAILED) {
munmap(const_cast<char*>(data_), size_); munmap(const_cast<char *>(data_), size_);
} }
if (fd_ >= 0) { if (fd_ >= 0) {
::close(fd_); ::close(fd_);
} }
} }
MappedFile::MappedFile(MappedFile&& other) noexcept MappedFile::MappedFile(MappedFile &&other) noexcept
: data_(other.data_), size_(other.size_), fd_(other.fd_) { : data_(other.data_), size_(other.size_), fd_(other.fd_) {
other.data_ = nullptr; other.data_ = nullptr;
other.fd_ = -1; other.fd_ = -1;
} }
MappedFile& MappedFile::operator=(MappedFile&& other) noexcept { MappedFile &MappedFile::operator=(MappedFile &&other) noexcept {
if (this != &other) { if (this != &other) {
if (data_ && data_ != MAP_FAILED) { if (data_ && data_ != MAP_FAILED) {
munmap(const_cast<char*>(data_), size_); munmap(const_cast<char *>(data_), size_);
} }
if (fd_ >= 0) { if (fd_ >= 0) {
::close(fd_); ::close(fd_);

View File

@ -12,28 +12,28 @@ Connects to:
#pragma once #pragma once
#include "src/core/Concepts.hpp"
#include <cstddef> #include <cstddef>
#include <expected> #include <expected>
#include <string_view> #include <string_view>
#include "src/core/Concepts.hpp"
class MappedFile { class MappedFile {
public: public:
MappedFile() = default; MappedFile() = default;
static std::expected<MappedFile, CrackError> open(std::string_view path); static std::expected<MappedFile, CrackError> open(std::string_view path);
~MappedFile(); ~MappedFile();
MappedFile(MappedFile&& other) noexcept; MappedFile(MappedFile &&other) noexcept;
MappedFile& operator=(MappedFile&& other) noexcept; MappedFile &operator=(MappedFile &&other) noexcept;
MappedFile(const MappedFile&) = delete; MappedFile(const MappedFile &) = delete;
MappedFile& operator=(const MappedFile&) = delete; MappedFile &operator=(const MappedFile &) = delete;
const char* data() const { return data_; } const char *data() const { return data_; }
std::size_t size() const { return size_; } std::size_t size() const { return size_; }
private: private:
const char* data_ = nullptr; const char *data_ = nullptr;
std::size_t size_ = 0; std::size_t size_ = 0;
int fd_ = -1; int fd_ = -1;
}; };

View File

@ -39,13 +39,13 @@ Connects to:
#include <ranges> #include <ranges>
#include <utility> #include <utility>
static constexpr std::array<std::pair<char, char>, 6> LEET_MAP = {{ static constexpr std::array<std::pair<char, char>, 6> LEET_MAP = {
{'a', '@'}, {'e', '3'}, {'i', '1'}, {{'a', '@'}, {'e', '3'}, {'i', '1'}, {'o', '0'}, {'s', '$'}, {'t', '7'}}};
{'o', '0'}, {'s', '$'}, {'t', '7'}
}};
std::generator<std::string> RuleSet::capitalize_first(std::string_view word) { std::generator<std::string> RuleSet::capitalize_first(std::string_view word) {
if (word.empty()) { co_return; } if (word.empty()) {
co_return;
}
std::string result(word); std::string result(word);
result[0] = static_cast<char>(std::toupper(static_cast<unsigned char>(result[0]))); result[0] = static_cast<char>(std::toupper(static_cast<unsigned char>(result[0])));
co_yield std::move(result); co_yield std::move(result);
@ -53,18 +53,20 @@ std::generator<std::string> RuleSet::capitalize_first(std::string_view word) {
std::generator<std::string> RuleSet::uppercase_all(std::string_view word) { std::generator<std::string> RuleSet::uppercase_all(std::string_view word) {
std::string result(word); std::string result(word);
std::ranges::transform(result, result.begin(), [](unsigned char c) { std::ranges::transform(result, result.begin(),
return static_cast<char>(std::toupper(c)); [](unsigned char c) { return static_cast<char>(std::toupper(c)); });
});
co_yield std::move(result); co_yield std::move(result);
} }
std::generator<std::string> RuleSet::leet_speak(std::string_view word) { std::generator<std::string> RuleSet::leet_speak(std::string_view word) {
std::string result(word); std::string result(word);
for (auto& c : result) { for (auto &c : result) {
auto lower = static_cast<char>(std::tolower(static_cast<unsigned char>(c))); auto lower = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
for (auto [from, to] : LEET_MAP) { for (auto [from, to] : LEET_MAP) {
if (lower == from) { c = to; break; } if (lower == from) {
c = to;
break;
}
} }
} }
co_yield std::move(result); co_yield std::move(result);
@ -92,7 +94,9 @@ std::generator<std::string> RuleSet::reverse(std::string_view word) {
std::generator<std::string> RuleSet::toggle_case(std::string_view word) { std::generator<std::string> RuleSet::toggle_case(std::string_view word) {
std::string result(word); std::string result(word);
std::ranges::transform(result, result.begin(), [](unsigned char c) { std::ranges::transform(result, result.begin(), [](unsigned char c) {
if (std::islower(c)) { return static_cast<char>(std::toupper(c)); } if (std::islower(c)) {
return static_cast<char>(std::toupper(c));
}
return static_cast<char>(std::tolower(c)); return static_cast<char>(std::tolower(c));
}); });
co_yield std::move(result); co_yield std::move(result);

View File

@ -17,7 +17,7 @@ Connects to:
#include <string_view> #include <string_view>
class RuleSet { class RuleSet {
public: public:
static std::generator<std::string> capitalize_first(std::string_view word); static std::generator<std::string> capitalize_first(std::string_view word);
static std::generator<std::string> uppercase_all(std::string_view word); static std::generator<std::string> uppercase_all(std::string_view word);
static std::generator<std::string> leet_speak(std::string_view word); static std::generator<std::string> leet_speak(std::string_view word);

View File

@ -27,18 +27,17 @@ void SharedState::set_result(std::string plaintext) {
} }
ThreadPool::ThreadPool(unsigned thread_count) ThreadPool::ThreadPool(unsigned thread_count)
: thread_count_(thread_count > 0 ? thread_count : thread_count_(thread_count > 0 ? thread_count : std::thread::hardware_concurrency()) {}
: std::thread::hardware_concurrency()) {}
void ThreadPool::run(WorkFn work) { void ThreadPool::run(WorkFn work) {
std::vector<std::jthread> threads; std::vector<std::jthread> threads;
threads.reserve(thread_count_); threads.reserve(thread_count_);
for (unsigned i = 0; i < thread_count_; ++i) { for (unsigned i = 0; i < thread_count_; ++i) {
threads.emplace_back([this, &work, i] { threads.emplace_back([this, &work, i] { work(i, thread_count_, state_); });
work(i, thread_count_, state_);
});
} }
} }
SharedState& ThreadPool::state() { return state_; } SharedState &ThreadPool::state() {
return state_;
}

View File

@ -43,15 +43,15 @@ struct SharedState {
}; };
class ThreadPool { class ThreadPool {
public: public:
using WorkFn = std::function<void(unsigned thread_id, unsigned total, SharedState&)>; using WorkFn = std::function<void(unsigned thread_id, unsigned total, SharedState &)>;
explicit ThreadPool(unsigned thread_count); explicit ThreadPool(unsigned thread_count);
void run(WorkFn work); void run(WorkFn work);
SharedState& state(); SharedState &state();
private: private:
unsigned thread_count_; unsigned thread_count_;
SharedState state_; SharedState state_;
}; };

View File

@ -13,8 +13,8 @@ Connects to:
attack/BruteForceAttack.hpp - BruteForceAttack tested attack/BruteForceAttack.hpp - BruteForceAttack tested
*/ */
#include <gtest/gtest.h>
#include "src/attack/BruteForceAttack.hpp" #include "src/attack/BruteForceAttack.hpp"
#include <gtest/gtest.h>
#include <set> #include <set>
#include <vector> #include <vector>
@ -42,8 +42,12 @@ TEST(BruteForceAttackTest, PartitionsKeyspace) {
BruteForceAttack p0("ab", 2, 0, 2); BruteForceAttack p0("ab", 2, 0, 2);
BruteForceAttack p1("ab", 2, 1, 2); BruteForceAttack p1("ab", 2, 1, 2);
std::set<std::string> all; std::set<std::string> all;
while (auto c = p0.next()) { all.insert(*c); } while (auto c = p0.next()) {
while (auto c = p1.next()) { all.insert(*c); } all.insert(*c);
}
while (auto c = p1.next()) {
all.insert(*c);
}
EXPECT_EQ(all.size(), 6); EXPECT_EQ(all.size(), 6);
} }

View File

@ -14,8 +14,8 @@ Connects to:
tests/data/small_wordlist.txt - fixture wordlist tests/data/small_wordlist.txt - fixture wordlist
*/ */
#include <gtest/gtest.h>
#include "src/attack/DictionaryAttack.hpp" #include "src/attack/DictionaryAttack.hpp"
#include <gtest/gtest.h>
#include <vector> #include <vector>
TEST(DictionaryAttackTest, ReadsAllWords) { TEST(DictionaryAttackTest, ReadsAllWords) {
@ -38,8 +38,12 @@ TEST(DictionaryAttackTest, PartitionsAcrossThreads) {
ASSERT_TRUE(p1.has_value()); ASSERT_TRUE(p1.has_value());
std::size_t count0 = 0, count1 = 0; std::size_t count0 = 0, count1 = 0;
while (p0->next()) { ++count0; } while (p0->next()) {
while (p1->next()) { ++count1; } ++count0;
}
while (p1->next()) {
++count1;
}
EXPECT_EQ(count0 + count1, 10); EXPECT_EQ(count0 + count1, 10);
} }

View File

@ -16,14 +16,13 @@ Connects to:
tests/data/small_wordlist.txt - fixture wordlist tests/data/small_wordlist.txt - fixture wordlist
*/ */
#include <gtest/gtest.h>
#include "src/core/Engine.hpp" #include "src/core/Engine.hpp"
#include "src/hash/SHA256Hasher.hpp" #include "src/hash/SHA256Hasher.hpp"
#include <gtest/gtest.h>
TEST(EngineTest, CracksSHA256WithDictionary) { TEST(EngineTest, CracksSHA256WithDictionary) {
CrackConfig cfg; CrackConfig cfg;
cfg.target_hash = cfg.target_hash = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8";
"5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8";
cfg.wordlist_path = "tests/data/small_wordlist.txt"; cfg.wordlist_path = "tests/data/small_wordlist.txt";
cfg.thread_count = 2; cfg.thread_count = 2;

View File

@ -13,8 +13,8 @@ Connects to:
core/Concepts.hpp - CrackError enum for error assertions core/Concepts.hpp - CrackError enum for error assertions
*/ */
#include <gtest/gtest.h>
#include "src/hash/HashDetector.hpp" #include "src/hash/HashDetector.hpp"
#include <gtest/gtest.h>
TEST(HashDetectorTest, DetectsMD5) { TEST(HashDetectorTest, DetectsMD5) {
auto result = HashDetector::detect("d41d8cd98f00b204e9800998ecf8427e"); auto result = HashDetector::detect("d41d8cd98f00b204e9800998ecf8427e");
@ -29,8 +29,8 @@ TEST(HashDetectorTest, DetectsSHA1) {
} }
TEST(HashDetectorTest, DetectsSHA256) { TEST(HashDetectorTest, DetectsSHA256) {
auto result = HashDetector::detect( auto result =
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); HashDetector::detect("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
ASSERT_TRUE(result.has_value()); ASSERT_TRUE(result.has_value());
EXPECT_EQ(*result, HashType::SHA256); EXPECT_EQ(*result, HashType::SHA256);
} }

View File

@ -16,20 +16,19 @@ Connects to:
hash/EVPHasher.hpp - underlying implementation hash/EVPHasher.hpp - underlying implementation
*/ */
#include <gtest/gtest.h>
#include "src/hash/MD5Hasher.hpp" #include "src/hash/MD5Hasher.hpp"
#include "src/hash/SHA1Hasher.hpp" #include "src/hash/SHA1Hasher.hpp"
#include "src/hash/SHA256Hasher.hpp" #include "src/hash/SHA256Hasher.hpp"
#include "src/hash/SHA512Hasher.hpp" #include "src/hash/SHA512Hasher.hpp"
#include <gtest/gtest.h>
TEST(SHA256HasherTest, KnownVectors) { TEST(SHA256HasherTest, KnownVectors) {
SHA256Hasher hasher; SHA256Hasher hasher;
EXPECT_EQ(hasher.hash(""), EXPECT_EQ(hasher.hash(""), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
EXPECT_EQ(hasher.hash("password"), EXPECT_EQ(hasher.hash("password"),
"5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"); "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8");
EXPECT_EQ(hasher.hash("hello"), EXPECT_EQ(hasher.hash("hello"),
"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"); "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824");
} }
TEST(SHA256HasherTest, StaticProperties) { TEST(SHA256HasherTest, StaticProperties) {
@ -68,12 +67,11 @@ TEST(SHA1HasherTest, StaticProperties) {
TEST(SHA512HasherTest, KnownVectors) { TEST(SHA512HasherTest, KnownVectors) {
SHA512Hasher hasher; SHA512Hasher hasher;
EXPECT_EQ(hasher.hash(""), EXPECT_EQ(hasher.hash(""), "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce"
"cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce" "47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e");
"47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e");
EXPECT_EQ(hasher.hash("password"), EXPECT_EQ(hasher.hash("password"),
"b109f3bbbc244eb82441917ed06d618b9008dd09b3befd1b5e07394c706a8bb9" "b109f3bbbc244eb82441917ed06d618b9008dd09b3befd1b5e07394c706a8bb9"
"80b1d7785e5976ec049b46df5f1326af5a2ea6d103fd07c95385ffab0cacbc86"); "80b1d7785e5976ec049b46df5f1326af5a2ea6d103fd07c95385ffab0cacbc86");
} }
TEST(SHA512HasherTest, StaticProperties) { TEST(SHA512HasherTest, StaticProperties) {

View File

@ -17,18 +17,15 @@ Connects to:
tests/data/small_wordlist.txt - fixture wordlist tests/data/small_wordlist.txt - fixture wordlist
*/ */
#include <gtest/gtest.h>
#include "src/attack/RuleAttack.hpp" #include "src/attack/RuleAttack.hpp"
#include "src/rules/RuleSet.hpp" #include "src/rules/RuleSet.hpp"
#include <algorithm> #include <algorithm>
#include <gtest/gtest.h>
#include <ranges>
#include <vector> #include <vector>
static std::vector<std::string> collect(std::generator<std::string> gen) { static std::vector<std::string> collect(std::generator<std::string> gen) {
std::vector<std::string> out; return std::ranges::to<std::vector<std::string>>(std::move(gen));
for (auto&& s : gen) {
out.push_back(std::move(s));
}
return out;
} }
TEST(RuleSetTest, CapitalizeFirst) { TEST(RuleSetTest, CapitalizeFirst) {
@ -103,10 +100,14 @@ TEST(RuleAttackTest, ChainRulesProducesMoreCandidates) {
ASSERT_TRUE(with_chain.has_value()); ASSERT_TRUE(with_chain.has_value());
std::size_t count_without = 0; std::size_t count_without = 0;
while (without->next()) { ++count_without; } while (without->next()) {
++count_without;
}
std::size_t count_with = 0; std::size_t count_with = 0;
while (with_chain->next()) { ++count_with; } while (with_chain->next()) {
++count_with;
}
EXPECT_GT(count_with, count_without); EXPECT_GT(count_with, count_without);
} }

View File

@ -0,0 +1,10 @@
# ©AngelaMos | 2026
# .shellcheckrc
#
# Disabled rules document false-positive systematic patterns in this project
# SC2034: CURRENT_TEST / SYSROOT pattern read by test framework via introspection
# SC1090: dynamic source paths in test runner (intentional discovery)
# SC1091: helper-file sources resolved at runtime, not visible to shellcheck
disable=SC2034
disable=SC1090
disable=SC1091

View File

@ -333,6 +333,7 @@ check_4_2_3() {
fi fi
local file_mode local file_mode
# shellcheck disable=SC2016 # literal $FileCreateMode and $2 are regex/awk syntax
file_mode=$(grep -E '^\s*\$FileCreateMode' "$rsyslog_conf" | tail -1 | awk '{print $2}') || true file_mode=$(grep -E '^\s*\$FileCreateMode' "$rsyslog_conf" | tail -1 | awk '{print $2}') || true
if [[ -z "$file_mode" ]]; then if [[ -z "$file_mode" ]]; then
@ -369,6 +370,7 @@ check_4_2_4() {
evidence="Found ${rule_count} logging rule(s) in rsyslog.conf" evidence="Found ${rule_count} logging rule(s) in rsyslog.conf"
else else
local include_count local include_count
# shellcheck disable=SC2016 # literal $IncludeConfig is regex syntax, not shell
include_count=$(grep -cE '^\s*\$IncludeConfig|^\s*include\(' "$rsyslog_conf") || true include_count=$(grep -cE '^\s*\$IncludeConfig|^\s*include\(' "$rsyslog_conf") || true
if [[ "$include_count" -gt 0 ]]; then if [[ "$include_count" -gt 0 ]]; then

View File

@ -110,6 +110,7 @@ emit_json_report() {
local status="${RESULT_STATUS[$id]}" local status="${RESULT_STATUS[$id]}"
local evidence="${RESULT_EVIDENCE[$id]:-}" local evidence="${RESULT_EVIDENCE[$id]:-}"
local title="${CTRL_TITLE[$id]}" local title="${CTRL_TITLE[$id]}"
# shellcheck disable=SC2153 # CTRL_SECTION is a populated global associative array
local ctrl_section="${CTRL_SECTION[$id]}" local ctrl_section="${CTRL_SECTION[$id]}"
local level="${CTRL_LEVEL[$id]}" local level="${CTRL_LEVEL[$id]}"
local scored="${CTRL_SCORED[$id]}" local scored="${CTRL_SCORED[$id]}"

View File

@ -25,7 +25,7 @@ fail() { echo -e "${RED}[✖]${RESET} $1" >&2; exit "$EXIT_FAIL"; }
progress() { progress() {
[[ "$QUIET" == "true" ]] && return [[ "$QUIET" == "true" ]] && return
printf "\r${DIM}[%s] Checking: %s${RESET}%s" "$1" "$2" "$(printf '%40s')" >&2 printf "\r${DIM}[%s] Checking: %s${RESET}%s" "$1" "$2" "$(printf '%40s' '')" >&2
} }
clear_progress() { clear_progress() {
@ -120,6 +120,7 @@ service_is_active() {
} }
package_is_installed() { package_is_installed() {
# shellcheck disable=SC2016 # ${Status} is dpkg-query template syntax, not shell expansion
if run_cmd dpkg-query -W -f='${Status}' "$1" | grep -q "install ok installed"; then if run_cmd dpkg-query -W -f='${Status}' "$1" | grep -q "install ok installed"; then
return 0 return 0
fi fi

View File

@ -138,11 +138,11 @@ test_baseline_missing_file() {
CURRENT_TEST="test_baseline_missing_file" CURRENT_TEST="test_baseline_missing_file"
setup_test "${PROJECT_DIR}/testdata/fixtures" setup_test "${PROJECT_DIR}/testdata/fixtures"
local result local result rc
result=$(load_baseline "/tmp/nonexistent_baseline_12345.json" 2>&1) || true result=$(load_baseline "/tmp/nonexistent_baseline_12345.json" 2>&1) && rc=0 || rc=$?
((TEST_TOTAL++)) || true ((TEST_TOTAL++)) || true
if [[ $? -eq 0 ]] || echo "$result" | grep -q "not found"; then if (( rc == 0 )) || echo "$result" | grep -q "not found"; then
((TEST_PASS++)) || true ((TEST_PASS++)) || true
else else
((TEST_FAIL++)) || true ((TEST_FAIL++)) || true

View File

@ -10,7 +10,7 @@ class View {
private: private:
static ftxui::Element render_header(const StatsSnapshot &data, const std::string &interface, static ftxui::Element render_header(const StatsSnapshot &data, const std::string &interface,
const std::string &filter); const std::string &filter);
static ftxui::Element render_stats(const StatsSnapshot &data); static ftxui::Element render_stats(const StatsSnapshot &data);
static ftxui::Element render_transport(const StatsSnapshot &data); static ftxui::Element render_transport(const StatsSnapshot &data);

View File

@ -21,8 +21,8 @@ class IP_class {
std::string dst; std::string dst;
public: public:
std::string get_source(); const std::string &get_source() const;
std::string get_dest(); const std::string &get_dest() const;
// getters // getters
/*virtual std::string get_source() = 0; /*virtual std::string get_source() = 0;
virtual std::string get_dest() = 0;*/ virtual std::string get_dest() = 0;*/

View File

@ -39,8 +39,8 @@ int main(int argc, char **argv) {
/* get a filter, use vector for multiple */ /* get a filter, use vector for multiple */
std::vector<filter> filters; std::vector<filter> filters;
if (parser.vm.contains("filter")) { if (parser.vm.contains("filter")) {
auto &f = parser.vm["filter"].as<std::vector<std::string>>(); const auto &f = parser.vm["filter"].as<std::vector<std::string>>();
for (auto &x : f) { for (const auto &x : f) {
filters.push_back(parse(x)); filters.push_back(parse(x));
filterString += x + " "; filterString += x + " ";
} }
@ -82,13 +82,10 @@ int main(int argc, char **argv) {
/* our class for UI */ /* our class for UI */
View view; View view;
std::mutex render_mtx; std::mutex render_mtx;
std::mutex event_mtx;
ftxui::Element current_render = isOffline ftxui::Element current_render = isOffline
? view.render(stats.get_snapshot(), interface, filterString, true, timer.load()) ? view.render(stats.get_snapshot(), interface, filterString, true, timer.load())
: ftxui::text("Starting capture..."); : ftxui::text("Starting capture...");
std::mutex screen_mtx;
auto component = ftxui::Renderer([&] { auto component = ftxui::Renderer([&] {
std::lock_guard<std::mutex> lock(render_mtx); std::lock_guard<std::mutex> lock(render_mtx);
return current_render; return current_render;

View File

@ -161,7 +161,7 @@ ftxui::Element View::render_bandwidth(const StatsSnapshot &data) {
const double frac = idx_f - static_cast<double>(i0); const double frac = idx_f - static_cast<double>(i0);
const double bw = data.bandwidth_history[i0].bytes_per_sec * (1.0 - frac) + const double bw = data.bandwidth_history[i0].bytes_per_sec * (1.0 - frac) +
data.bandwidth_history[i1].bytes_per_sec * frac; data.bandwidth_history[i1].bytes_per_sec * frac;
const double v = bw / max_bw; const double v = bw / max_bw;
output[x] = static_cast<int>(v * static_cast<double>(height - 1)); output[x] = static_cast<int>(v * static_cast<double>(height - 1));

View File

@ -112,7 +112,7 @@ void PcapCapture::stop() {
/* print all available interfaces */ /* print all available interfaces */
void PcapCapture::print_interfaces() { void PcapCapture::print_interfaces() {
int i = 0; int i = 0;
for (pcap_if_t *dev = interfaces; dev != nullptr; dev = dev->next) { for (const pcap_if_t *dev = interfaces; dev != nullptr; dev = dev->next) {
printf("%d. %s ", ++i, dev->name); printf("%d. %s ", ++i, dev->name);
if (dev->description != nullptr) { if (dev->description != nullptr) {
printf("(%s)\n", dev->description); printf("(%s)\n", dev->description);
@ -132,7 +132,7 @@ void PcapCapture::print_interfaces() {
* @param packet Raw packet bytes * @param packet Raw packet bytes
*/ */
void PcapCapture::callback(u_char *user, const struct pcap_pkthdr *header, const u_char *packet) { void PcapCapture::callback(u_char *user, const struct pcap_pkthdr *header, const u_char *packet) {
auto * const self = reinterpret_cast<PcapCapture *>(user); auto *const self = reinterpret_cast<PcapCapture *>(user);
if (!self->isRunning()) { if (!self->isRunning()) {
return; return;
} }

View File

@ -11,13 +11,12 @@
uint16_t IP_class::get_payload_len() const { return payload_len; } uint16_t IP_class::get_payload_len() const { return payload_len; }
TransportProtocol IP_class::get_protocol() const { return protocol; } TransportProtocol IP_class::get_protocol() const { return protocol; }
std::string IP_class::get_source() { return src; } const std::string &IP_class::get_source() const { return src; }
std::string IP_class::get_dest() { return dst; } const std::string &IP_class::get_dest() const { return dst; }
/*** Ipv4 ***/ /*** Ipv4 ***/
IPv4::IPv4(const u_char *data) IPv4::IPv4(const u_char *data)
: ip_hdr(reinterpret_cast<const ip *>(data)), : ip_hdr(reinterpret_cast<const ip *>(data)), ip_hdr_len(static_cast<int>(ip_hdr->ip_hl) * 4) {
ip_hdr_len(static_cast<int>(ip_hdr->ip_hl) * 4) {
std::array<char, INET_ADDRSTRLEN> src_buf{}; std::array<char, INET_ADDRSTRLEN> src_buf{};
inet_ntop(AF_INET, &ip_hdr->ip_src, src_buf.data(), sizeof(src_buf)); inet_ntop(AF_INET, &ip_hdr->ip_src, src_buf.data(), sizeof(src_buf));
src = src_buf.data(); src = src_buf.data();
@ -84,8 +83,7 @@ uint16_t IPv4::get_dest_port() { return dest_port; }
/*** Ipv6 ***/ /*** Ipv6 ***/
IPv6::IPv6(const u_char *data) IPv6::IPv6(const u_char *data)
: ip_hdr(reinterpret_cast<const ip6_hdr *>(data)), : ip_hdr(reinterpret_cast<const ip6_hdr *>(data)), ptr(reinterpret_cast<const uint8_t *>(ip_hdr + 1)) {
ptr(reinterpret_cast<const uint8_t *>(ip_hdr + 1)) {
uint8_t hdr = ip_hdr->ip6_nxt; uint8_t hdr = ip_hdr->ip6_nxt;
std::array<char, INET6_ADDRSTRLEN> src{}; std::array<char, INET6_ADDRSTRLEN> src{};
inet_ntop(AF_INET6, &ip_hdr->ip6_src, src.data(), sizeof(src)); inet_ntop(AF_INET6, &ip_hdr->ip6_src, src.data(), sizeof(src));

View File

@ -15,7 +15,7 @@ ApplicationProtocol Packet::get_application_protocol() {
return ApplicationProtocol::DNS; return ApplicationProtocol::DNS;
} }
if (transport_protocol == TransportProtocol::TCP && payload_len >= 3) { if (transport_protocol == TransportProtocol::TCP) {
if (payload_ptr[0] == 0x16 && payload_ptr[1] == 0x03) { if (payload_ptr[0] == 0x16 && payload_ptr[1] == 0x03) {
return ApplicationProtocol::HTTPS; return ApplicationProtocol::HTTPS;
} }

View File

@ -2,7 +2,7 @@
#include "ftxui/dom/table.hpp" #include "ftxui/dom/table.hpp"
#include <fstream> #include <fstream>
Stats::Stats() { last_tick = std::chrono::steady_clock::now(); } Stats::Stats() : last_tick(std::chrono::steady_clock::now()) {}
/** /**
* @brief Aggregates a newly captured packet. * @brief Aggregates a newly captured packet.
* *
@ -96,7 +96,7 @@ void Stats::update_transport_stats() {
snapshot.transport_rows.push_back({"Proto", "Packets", "Bytes", "%"}); snapshot.transport_rows.push_back({"Proto", "Packets", "Bytes", "%"});
std::vector<std::pair<TransportProtocol, protocolStats>> tps(transport_map.begin(), transport_map.end()); std::vector<std::pair<TransportProtocol, protocolStats>> tps(transport_map.begin(), transport_map.end());
std::sort(tps.begin(), tps.end(), [](auto &a, auto &b) { return a.second.packets > b.second.packets; }); std::sort(tps.begin(), tps.end(), [](const auto &a, const auto &b) { return a.second.packets > b.second.packets; });
for (const auto &[proto, stats] : tps) { for (const auto &[proto, stats] : tps) {
double percent = snapshot.total_b ? stats.bytes * 100.0 / snapshot.total_b : 0.0; double percent = snapshot.total_b ? stats.bytes * 100.0 / snapshot.total_b : 0.0;
@ -116,7 +116,8 @@ void Stats::update_application_stats() {
std::lock_guard<std::mutex> lock(mtx); std::lock_guard<std::mutex> lock(mtx);
std::vector<std::pair<ApplicationProtocol, protocolStats>> apps(application_map.begin(), application_map.end()); std::vector<std::pair<ApplicationProtocol, protocolStats>> apps(application_map.begin(), application_map.end());
std::sort(apps.begin(), apps.end(), [](auto &a, auto &b) { return a.second.packets > b.second.packets; }); std::sort(apps.begin(), apps.end(),
[](const auto &a, const auto &b) { return a.second.packets > b.second.packets; });
snapshot.app_rows.clear(); snapshot.app_rows.clear();
snapshot.app_rows.push_back({"Proto", "Packets", "Bytes (MB)", "%"}); snapshot.app_rows.push_back({"Proto", "Packets", "Bytes (MB)", "%"});
@ -144,7 +145,8 @@ void Stats::update_ip_stats(size_t limit) {
snapshot.rows.push_back({"IP Address", "Packets TX", "Packets RX"}); snapshot.rows.push_back({"IP Address", "Packets TX", "Packets RX"});
std::vector<std::pair<std::string, IPStats>> ips(ip_map.begin(), ip_map.end()); std::vector<std::pair<std::string, IPStats>> ips(ip_map.begin(), ip_map.end());
std::sort(ips.begin(), ips.end(), [](auto &a, auto &b) { return a.second.packets_sent > b.second.packets_sent; }); std::sort(ips.begin(), ips.end(),
[](const auto &a, const auto &b) { return a.second.packets_sent > b.second.packets_sent; });
size_t count = 0; size_t count = 0;
for (const auto &[ip, s] : ips) { for (const auto &[ip, s] : ips) {
@ -166,7 +168,7 @@ void Stats::update_ip_stats(size_t limit) {
void Stats::update_pairs(size_t limit) { void Stats::update_pairs(size_t limit) {
std::lock_guard<std::mutex> lock(mtx); std::lock_guard<std::mutex> lock(mtx);
std::vector<std::pair<std::pair<std::string, std::string>, protocolStats>> vec(pairs.begin(), pairs.end()); std::vector<std::pair<std::pair<std::string, std::string>, protocolStats>> vec(pairs.begin(), pairs.end());
std::sort(vec.begin(), vec.end(), [](auto &a, auto &b) { return a.second.bytes > b.second.bytes; }); std::sort(vec.begin(), vec.end(), [](const auto &a, const auto &b) { return a.second.bytes > b.second.bytes; });
snapshot.pairs_rows.clear(); snapshot.pairs_rows.clear();
snapshot.pairs_rows.push_back({"Source", "Destination", "bytes received", "%"}); snapshot.pairs_rows.push_back({"Source", "Destination", "bytes received", "%"});

View File

@ -4,41 +4,42 @@ namespace po = boost::program_options;
int main(int argc, char* argv[]) { int main(int argc, char* argv[]) {
po::options_description desc("Allowed options"); po::options_description desc("Allowed options");
desc.add_options() desc.add_options()("help,h", "produce help message")(
("help,h", "produce help message") "dname,i", po::value<std::string>()->default_value("127.0.0.1"),
("dname,i", po::value<std::string>()->default_value("127.0.0.1"), "set domain name or IP address") "set domain name or IP address")("ports,p",
("ports,p", po::value<std::string>()->default_value("1-1024"), "set a port range from 1 to n") po::value<std::string>()->default_value("1-1024"),
("threads,t", po::value<int>()->default_value(100), "max concurrent threads") "set a port range from 1 to n")(
("expiry_time,e", po::value<uint8_t>()->default_value(2)->value_name("sec"), "timeout in seconds") "threads,t", po::value<int>()->default_value(100), "max concurrent threads")(
("verbose,v", "verbose output"); "expiry_time,e", po::value<uint8_t>()->default_value(2)->value_name("sec"),
"timeout in seconds")("verbose,v", "verbose output");
po::variables_map vm; po::variables_map vm;
po::store(po::parse_command_line(argc, argv, desc), vm); po::store(po::parse_command_line(argc, argv, desc), vm);
po::notify(vm); po::notify(vm);
if (vm.count("help")) { if (vm.count("help")) {
std::cout << desc << "\n"; std::cout << desc << "\n";
std::cout << "Examples:\n" std::cout << "Examples:\n"
<< " Scan common ports on localhost:\n" << " Scan common ports on localhost:\n"
<< " ./port_scanner -i 127.0.0.1 -p 1-1024\n\n" << " ./port_scanner -i 127.0.0.1 -p 1-1024\n\n"
<< " Full TCP port scan:\n" << " Full TCP port scan:\n"
<< " ./port_scanner -i 192.168.1.1 -p 65535 -t 200\n\n" << " ./port_scanner -i 192.168.1.1 -p 65535 -t 200\n\n"
<< " Scan with custom timeout:\n" << " Scan with custom timeout:\n"
<< " ./port_scanner -i example.com -p 80-443 -e 5\n\n" << " ./port_scanner -i example.com -p 80-443 -e 5\n\n"
<< " Postscriptum:\n" << " Postscriptum:\n"
<< " Scan only systems you own or have explicit permission to test.\n"; << " Scan only systems you own or have explicit permission to test.\n";
return 0; return 0;
} }
std::string ip = vm["dname"].as<std::string>(); std::string ip = vm["dname"].as<std::string>();
std::string port = vm["ports"].as<std::string>(); std::string port = vm["ports"].as<std::string>();
int threads = vm["threads"].as<int>(); int threads = vm["threads"].as<int>();
uint8_t expiry_time = vm["expiry_time"].as<uint8_t>(); uint8_t expiry_time = vm["expiry_time"].as<uint8_t>();
PortScanner scanner; PortScanner scanner;
scanner.set_options(ip, port, threads, expiry_time); scanner.set_options(ip, port, threads, expiry_time);
scanner.start(); scanner.start();
scanner.run(); scanner.run();
return 0; return 0;
} }

View File

@ -1,193 +1,174 @@
#include "PortScanner.hpp" #include "PortScanner.hpp"
const std::unordered_map<uint16_t, std::string> PortScanner::basicPorts{ const std::unordered_map<uint16_t, std::string> PortScanner::basicPorts{
{21, "FTP"}, {21, "FTP"}, {22, "SSH"}, {23, "TelNet"}, {25, "SMTP"}, {53, "DNS"},
{22, "SSH"}, {67, "DHCP server"}, {68, "DHCP client"}, {80, "HTTP"}, {110, "POP3"}, {143, "IMAP"},
{23, "TelNet"}, {161, "SNMP"}, {443, "HTTPS"}, {445, "SMB"}, {465, "SMTPS"}, {993, "IMAPS"},
{25, "SMTP"}, {1080, "SOCKS"}, {1521, "ORACLE DB"}, {3306, "MySQL"}, {3389, "RDP"}, {5432, "PostgreSQL"},
{53, "DNS"}, {6379, "Redis"},
{67, "DHCP server"},
{68, "DHCP client"},
{80, "HTTP"},
{110, "POP3"},
{143, "IMAP"},
{161, "SNMP"},
{443, "HTTPS"},
{445, "SMB"},
{465, "SMTPS"},
{993, "IMAPS"},
{1080, "SOCKS"},
{1521, "ORACLE DB"},
{3306, "MySQL"},
{3389, "RDP"},
{5432, "PostgreSQL"},
{6379, "Redis"},
}; };
void PortScanner::parse_port(std::string& port) { void PortScanner::parse_port(std::string& port) {
auto t = std::find(port.begin(), port.end(), '-'); auto t = std::find(port.begin(), port.end(), '-');
if (t == port.end()) { if (t == port.end()) {
startPort = 1; startPort = 1;
endPort = std::stoi(port); endPort = std::stoi(port);
return; return;
} }
auto it = port.begin(); auto it = port.begin();
std::string s = "", e = ""; std::string s = "", e = "";
while (it != port.end()) { while (it != port.end()) {
if (*it == '-') { if (*it == '-') {
break; break;
} }
s += *it; s += *it;
++it; ++it;
} }
++it; ++it;
while (it != port.end()) { while (it != port.end()) {
e += *it; e += *it;
++it; ++it;
} }
int start = std::stoi(s); int start_port = std::stoi(s);
int end = std::stoi(e); int end_port = std::stoi(e);
//check a valid bounds if (start_port == 0 || end_port > MAX_PORT || start_port > end_port) {
if (start == 0 || end > MAX_PORT || start > end) { startPort = 1;
startPort = 1; endPort = MAX_PORT;
endPort = MAX_PORT; } else {
} startPort = static_cast<uint16_t>(start_port);
else { endPort = static_cast<uint16_t>(end_port);
startPort = static_cast<uint16_t>(start); }
endPort = static_cast<uint16_t>(end);
}
} }
PortScanner::PortScanner(std::string& domainName, std::string& port, int max_threads, std::uint8_t expiry_time) { PortScanner::PortScanner(std::string& domainName, std::string& port, int max_threads,
this->domainName = std::move(domainName); std::uint8_t expiry_time) {
this->MAX_THREADS = max_threads; this->domainName = std::move(domainName);
this->expiry_time = expiry_time; this->MAX_THREADS = max_threads;
this->expiry_time = expiry_time;
parse_port(port); parse_port(port);
auto result = resolver.resolve(this->domainName, ""); auto result = resolver.resolve(this->domainName, "");
endpoint = *result.begin(); endpoint = *result.begin();
setup_queue();
setup_queue();
} }
void PortScanner::setup_queue() { void PortScanner::setup_queue() {
q = std::queue<uint16_t>(); q = std::queue<uint16_t>();
for (int i = startPort; i <= endPort; i++) { for (int i = startPort; i <= endPort; i++) {
q.push(i); q.push(i);
} }
} }
void PortScanner::set_options(std::string& domainName, std::string& port, int max_threads,
std::uint8_t expiry_time) {
this->domainName = std::move(domainName);
this->MAX_THREADS = max_threads;
this->expiry_time = expiry_time;
parse_port(port);
void PortScanner::set_options(std::string& domainName, std::string& port, int max_threads, std::uint8_t expiry_time) { auto result = resolver.resolve(this->domainName, "");
this->domainName = std::move(domainName); endpoint = *result.begin();
this->MAX_THREADS = max_threads;
this->expiry_time = expiry_time;
parse_port(port);
auto result = resolver.resolve(this->domainName, "");
endpoint = *result.begin();
} }
void PortScanner::set_max_port(std::uint16_t port) { void PortScanner::set_max_port(std::uint16_t port) {
endPort = port; endPort = port;
} }
void PortScanner::set_max_threads(int value) { void PortScanner::set_max_threads(int value) {
MAX_THREADS = value; MAX_THREADS = value;
} }
void PortScanner::set_ip_address(std::string ip) { void PortScanner::set_ip_address(std::string ip) {
domainName = std::move(ip); domainName = std::move(ip);
} }
void PortScanner::set_expiry_time(std::uint8_t value) { void PortScanner::set_expiry_time(std::uint8_t value) {
expiry_time = value; expiry_time = value;
} }
void PortScanner::start() { void PortScanner::start() {
setup_queue(); setup_queue();
for (int i = 0; i < MAX_THREADS; i++) { for (int i = 0; i < MAX_THREADS; i++) {
boost::asio::post(strand, [this]() { boost::asio::post(strand, [this]() { scan(); });
scan(); }
});
}
} }
void PortScanner::run() { void PortScanner::run() {
printf("PORT\tSTATE\tSERVICE\tBANNER\n"); printf("PORT\tSTATE\tSERVICE\tBANNER\n");
io.run(); io.run();
printf("\nResult:\n"); printf("\nResult:\n");
printf(" Open ports: %d\n", open_ports); printf(" Open ports: %d\n", open_ports);
printf(" Closed ports: %d\n", closed_ports); printf(" Closed ports: %d\n", closed_ports);
printf(" Filtered ports: %d\n", filtered_ports); printf(" Filtered ports: %d\n", filtered_ports);
} }
void PortScanner::scan() { void PortScanner::scan() {
if (q.empty() || cnt >= MAX_THREADS) return; if (q.empty() || cnt >= MAX_THREADS)
return;
uint16_t port = q.front(); uint16_t port = q.front();
q.pop(); q.pop();
++cnt; ++cnt;
auto socket = std::make_shared<tcp::socket>(io); auto socket = std::make_shared<tcp::socket>(io);
auto timer = std::make_shared<boost::asio::steady_timer>(io); auto timer = std::make_shared<boost::asio::steady_timer>(io);
auto complete = std::make_shared<bool>(false); auto complete = std::make_shared<bool>(false);
tcp::endpoint endpoint(this->endpoint.address(), port); tcp::endpoint target_endpoint(this->endpoint.address(), port);
timer->expires_after(std::chrono::seconds(expiry_time)); timer->expires_after(std::chrono::seconds(expiry_time));
timer->async_wait(boost::asio::bind_executor(strand, [this, complete, socket, port](boost::system::error_code ec) { timer->async_wait(boost::asio::bind_executor(
if (!ec && !*complete) { strand, [this, complete, socket, port](boost::system::error_code ec) {
*complete = true; if (!ec && !*complete) {
socket->close(); *complete = true;
printf("%i\t%s\t%s\t%s\n", port, "FILTERED", "NULL", "NULL"); socket->close();
++filtered_ports; printf("%i\t%s\t%s\t%s\n", port, "FILTERED", "NULL", "NULL");
--cnt; ++filtered_ports;
scan(); --cnt;
} scan();
}
}));
})); socket->async_connect(target_endpoint,
boost::asio::bind_executor(strand, [this, socket, timer, port, complete](
boost::system::error_code ec) {
if (*complete)
return;
*complete = true;
timer->cancel();
socket->async_connect(endpoint, boost::asio::bind_executor(strand, [this,socket, timer, port, complete](boost::system::error_code ec) { std::string service = "---";
if (*complete) return; auto banner = std::make_shared<std::string>("---");
*complete = true; auto it = basicPorts.find(port);
timer->cancel(); if (it != basicPorts.end()) {
service = it->second;
}
std::string service = "---"; if (!ec) {
auto banner = std::make_shared<std::string>("---"); auto buf = std::make_shared<std::array<char, 128>>();
auto it = basicPorts.find(port);
if (it != basicPorts.end()) {
service = it->second;
}
if (!ec) { socket->async_read_some(
auto buf = std::make_shared<std::array<char, 128>>(); boost::asio::buffer(*buf),
boost::asio::bind_executor(
socket->async_read_some(boost::asio::buffer(*buf),boost::asio::bind_executor(strand, strand, [this, port, buf, banner, service](
[this, port, buf, banner, service](boost::system::error_code ec, std::size_t n) { boost::system::error_code ec, std::size_t n) {
if (!ec && n > 0) { if (!ec && n > 0) {
banner->assign(buf->data(), n); banner->assign(buf->data(), n);
} }
printf("%i\t%sOPEN%s\t%s\t%s\n", port, GREEN,RESET, service.c_str(), banner->c_str()); printf("%i\t%sOPEN%s\t%s\t%s\n", port, GREEN, RESET,
++open_ports; service.c_str(), banner->c_str());
--cnt; ++open_ports;
scan(); --cnt;
})); scan();
}));
}
else{
printf("%i\t%sCLOSED%s\t%s\t%s\n", port, RED, RESET, service.c_str(), banner->c_str());
++closed_ports;
--cnt;
scan();
}
}));
} else {
printf("%i\t%sCLOSED%s\t%s\t%s\n", port, RED, RESET,
service.c_str(), banner->c_str());
++closed_ports;
--cnt;
scan();
}
}));
} }

View File

@ -1,58 +1,61 @@
#ifndef PORTSCANNER_HPP #ifndef PORTSCANNER_HPP
#define PORTSCANNER_HPP #define PORTSCANNER_HPP
#include <boost/asio.hpp>
#include <string>
#include <queue>
#include <unordered_map>
#include <iostream>
#include <array> #include <array>
#include <memory> #include <boost/asio.hpp>
#include <cstdint> #include <cstdint>
#include <iostream>
#include <memory>
#include <queue>
#include <stdio.h> #include <stdio.h>
#include <string>
#include <unordered_map>
#define RED "\033[31m" #define RED "\033[31m"
#define GREEN "\033[32m" #define GREEN "\033[32m"
#define RESET "\033[0m" #define RESET "\033[0m"
using boost::asio::ip::tcp; using boost::asio::ip::tcp;
class PortScanner { class PortScanner {
private: private:
static const std::unordered_map<uint16_t, std::string> basicPorts; static const std::unordered_map<uint16_t, std::string> basicPorts;
static const uint16_t MAX_PORT = 65535; static const uint16_t MAX_PORT = 65535;
boost::asio::io_context io; boost::asio::io_context io;
boost::asio::ip::tcp::resolver resolver{io}; boost::asio::ip::tcp::resolver resolver{io};
boost::asio::ip::tcp::endpoint endpoint; boost::asio::ip::tcp::endpoint endpoint;
boost::asio::strand<boost::asio::io_context::executor_type> strand{io.get_executor()}; boost::asio::strand<boost::asio::io_context::executor_type> strand{io.get_executor()};
std::queue<std::uint16_t> q; std::queue<std::uint16_t> q;
int cnt = 0; int cnt = 0;
int MAX_THREADS = 0; int MAX_THREADS = 0;
int open_ports = 0; int open_ports = 0;
int closed_ports = 0; int closed_ports = 0;
int filtered_ports = 0; int filtered_ports = 0;
std::string domainName; std::string domainName;
std::uint16_t startPort = 1; std::uint16_t startPort = 1;
std::uint16_t endPort = MAX_PORT; std::uint16_t endPort = MAX_PORT;
std::uint8_t expiry_time; std::uint8_t expiry_time;
void scan();
void setup_queue();
void parse_port(std::string& port);
void scan();
void setup_queue();
void parse_port(std::string& port);
public: public:
PortScanner(std::string& ip_address, std::string& port, int max_threads, std::uint8_t expiry_time); PortScanner(std::string& ip_address, std::string& port, int max_threads,
PortScanner(){} std::uint8_t expiry_time);
~PortScanner() { PortScanner() {
}
~PortScanner() {
}
} void set_options(std::string& domainName, std::string& port, int max_threads,
std::uint8_t expiry_time);
void set_max_port(std::uint16_t port);
void set_max_threads(int value);
void set_ip_address(std::string ip);
void set_expiry_time(std::uint8_t value);
void set_options(std::string& domainName, std::string& port, int max_threads, std::uint8_t expiry_time); void start();
void set_max_port(std::uint16_t port); void run();
void set_max_threads(int value);
void set_ip_address(std::string ip);
void set_expiry_time(std::uint8_t value);
void start();
void run();
}; };
#endif //PORTSCANNER_HPP #endif // PORTSCANNER_HPP

View File

@ -61,12 +61,7 @@ pub struct AnalysisContext {
} }
impl AnalysisContext { impl AnalysisContext {
pub fn new( pub fn new(source: BinarySource, sha256: String, file_name: String, file_size: u64) -> Self {
source: BinarySource,
sha256: String,
file_name: String,
file_size: u64,
) -> Self {
Self { Self {
source, source,
sha256, sha256,

View File

@ -25,10 +25,7 @@ pub enum EngineError {
UnsupportedArchitecture { arch: String }, UnsupportedArchitecture { arch: String },
#[error("pass '{pass}' missing dependency: {dependency}")] #[error("pass '{pass}' missing dependency: {dependency}")]
MissingDependency { MissingDependency { pass: String, dependency: String },
pass: String,
dependency: String,
},
#[error("pass '{pass}' failed")] #[error("pass '{pass}' failed")]
PassFailed { PassFailed {

View File

@ -25,31 +25,23 @@
// types.rs - Architecture, BinaryFormat, Endianness, // types.rs - Architecture, BinaryFormat, Endianness,
// SectionPermissions // SectionPermissions
use goblin::elf::Elf;
use goblin::elf::dynamic::DT_BIND_NOW; use goblin::elf::dynamic::DT_BIND_NOW;
use goblin::elf::header::{ use goblin::elf::header::{
EM_386, EM_AARCH64, EM_ARM, EM_X86_64, ET_CORE, ET_DYN, EM_386, EM_AARCH64, EM_ARM, EM_X86_64, ET_CORE, ET_DYN, ET_EXEC, ET_REL,
ET_EXEC, ET_REL,
}; };
use goblin::elf::program_header::{ use goblin::elf::program_header::{
PF_R, PF_W, PF_X, PT_DYNAMIC, PT_GNU_EH_FRAME, PF_R, PF_W, PF_X, PT_DYNAMIC, PT_GNU_EH_FRAME, PT_GNU_RELRO, PT_GNU_STACK, PT_INTERP, PT_LOAD,
PT_GNU_RELRO, PT_GNU_STACK, PT_INTERP, PT_LOAD, PT_NOTE, PT_NOTE, PT_NULL, PT_PHDR,
PT_NULL, PT_PHDR,
};
use goblin::elf::section_header::{
SHF_ALLOC, SHF_EXECINSTR, SHF_WRITE, SHT_NOBITS,
SHT_SYMTAB,
}; };
use goblin::elf::section_header::{SHF_ALLOC, SHF_EXECINSTR, SHF_WRITE, SHT_NOBITS, SHT_SYMTAB};
use goblin::elf::sym::STT_FUNC; use goblin::elf::sym::STT_FUNC;
use goblin::elf::Elf;
use super::{ use super::{
detect_common_anomalies, compute_section_hash, ElfInfo, ElfInfo, FormatResult, SectionInfo, SegmentInfo, compute_section_hash, detect_common_anomalies,
FormatResult, SectionInfo, SegmentInfo,
}; };
use crate::error::EngineError; use crate::error::EngineError;
use crate::types::{ use crate::types::{Architecture, BinaryFormat, Endianness, SectionPermissions};
Architecture, BinaryFormat, Endianness, SectionPermissions,
};
const EI_OSABI: usize = 7; const EI_OSABI: usize = 7;
const ELFOSABI_NONE: u8 = 0; const ELFOSABI_NONE: u8 = 0;
@ -65,12 +57,8 @@ const ELFOSABI_STANDALONE: u8 = 255;
const DT_FLAGS: u64 = 30; const DT_FLAGS: u64 = 30;
const DF_BIND_NOW: u64 = 0x8; const DF_BIND_NOW: u64 = 0x8;
pub fn parse_elf( pub fn parse_elf(elf: &Elf, data: &[u8]) -> Result<FormatResult, EngineError> {
elf: &Elf, let architecture = map_architecture(elf.header.e_machine);
data: &[u8],
) -> Result<FormatResult, EngineError> {
let architecture =
map_architecture(elf.header.e_machine);
let bits = if elf.is_64 { 64 } else { 32 }; let bits = if elf.is_64 { 64 } else { 32 };
let endianness = if elf.little_endian { let endianness = if elf.little_endian {
Endianness::Little Endianness::Little
@ -85,32 +73,20 @@ pub fn parse_elf(
.any(|sh| sh.sh_type == SHT_SYMTAB); .any(|sh| sh.sh_type == SHT_SYMTAB);
let is_stripped = !has_symtab; let is_stripped = !has_symtab;
let has_interp = elf let has_interp = elf.program_headers.iter().any(|ph| ph.p_type == PT_INTERP);
.program_headers let is_pie = elf.header.e_type == ET_DYN && has_interp;
.iter()
.any(|ph| ph.p_type == PT_INTERP);
let is_pie =
elf.header.e_type == ET_DYN && has_interp;
let has_debug_info = let has_debug_info = elf.section_headers.iter().any(|sh| {
elf.section_headers.iter().any(|sh| { elf.shdr_strtab
elf.shdr_strtab .get_at(sh.sh_name)
.get_at(sh.sh_name) .is_some_and(|name| name.starts_with(".debug_"))
.is_some_and(|name| { });
name.starts_with(".debug_")
})
});
let sections = build_sections(elf, data); let sections = build_sections(elf, data);
let segments = build_segments(elf); let segments = build_segments(elf);
let anomalies = detect_common_anomalies( let anomalies = detect_common_anomalies(&sections, entry_point, is_stripped);
&sections,
entry_point,
is_stripped,
);
let elf_info = build_elf_info(elf); let elf_info = build_elf_info(elf);
let function_hints = let function_hints = collect_function_hints(elf, entry_point);
collect_function_hints(elf, entry_point);
Ok(FormatResult { Ok(FormatResult {
format: BinaryFormat::Elf, format: BinaryFormat::Elf,
@ -137,11 +113,7 @@ fn map_architecture(machine: u16) -> Architecture {
EM_X86_64 => Architecture::X86_64, EM_X86_64 => Architecture::X86_64,
EM_ARM => Architecture::Arm, EM_ARM => Architecture::Arm,
EM_AARCH64 => Architecture::Aarch64, EM_AARCH64 => Architecture::Aarch64,
other => { other => Architecture::Other(format!("elf-machine-{other}")),
Architecture::Other(format!(
"elf-machine-{other}"
))
}
} }
} }
@ -188,10 +160,7 @@ fn segment_type_name(p_type: u32) -> Option<String> {
) )
} }
fn build_sections( fn build_sections(elf: &Elf, data: &[u8]) -> Vec<SectionInfo> {
elf: &Elf,
data: &[u8],
) -> Vec<SectionInfo> {
elf.section_headers elf.section_headers
.iter() .iter()
.skip(1) .skip(1)
@ -203,32 +172,16 @@ fn build_sections(
.to_string(); .to_string();
let is_nobits = shdr.sh_type == SHT_NOBITS; let is_nobits = shdr.sh_type == SHT_NOBITS;
let raw_offset = if is_nobits { let raw_offset = if is_nobits { 0 } else { shdr.sh_offset };
0 let raw_size = if is_nobits { 0 } else { shdr.sh_size };
} else {
shdr.sh_offset
};
let raw_size = if is_nobits {
0
} else {
shdr.sh_size
};
let permissions = SectionPermissions { let permissions = SectionPermissions {
read: (shdr.sh_flags read: (shdr.sh_flags & u64::from(SHF_ALLOC)) != 0,
& u64::from(SHF_ALLOC)) write: (shdr.sh_flags & u64::from(SHF_WRITE)) != 0,
!= 0, execute: (shdr.sh_flags & u64::from(SHF_EXECINSTR)) != 0,
write: (shdr.sh_flags
& u64::from(SHF_WRITE))
!= 0,
execute: (shdr.sh_flags
& u64::from(SHF_EXECINSTR))
!= 0,
}; };
let sha256 = compute_section_hash( let sha256 = compute_section_hash(data, raw_offset, raw_size);
data, raw_offset, raw_size,
);
SectionInfo { SectionInfo {
name, name,
@ -267,11 +220,9 @@ fn build_segments(elf: &Elf) -> Vec<SegmentInfo> {
} }
fn build_elf_info(elf: &Elf) -> ElfInfo { fn build_elf_info(elf: &Elf) -> ElfInfo {
let os_abi = let os_abi = os_abi_name(elf.header.e_ident[EI_OSABI]);
os_abi_name(elf.header.e_ident[EI_OSABI]);
let elf_type = elf_type_name(elf.header.e_type); let elf_type = elf_type_name(elf.header.e_type);
let interpreter = let interpreter = elf.interpreter.map(|s| s.to_string());
elf.interpreter.map(|s| s.to_string());
let gnu_relro = elf let gnu_relro = elf
.program_headers .program_headers
@ -287,23 +238,17 @@ fn build_elf_info(elf: &Elf) -> ElfInfo {
let mut bind_now = false; let mut bind_now = false;
if let Some(dynamic) = &elf.dynamic { if let Some(dynamic) = &elf.dynamic {
for dyn_entry in &dynamic.dyns { for dyn_entry in &dynamic.dyns {
let tag = dyn_entry.d_tag as u64; let tag = dyn_entry.d_tag;
if tag == DT_BIND_NOW { if tag == DT_BIND_NOW {
bind_now = true; bind_now = true;
} }
if tag == DT_FLAGS if tag == DT_FLAGS && (dyn_entry.d_val & DF_BIND_NOW) != 0 {
&& (dyn_entry.d_val & DF_BIND_NOW) != 0
{
bind_now = true; bind_now = true;
} }
} }
} }
let needed_libraries = elf let needed_libraries = elf.libraries.iter().map(|s| s.to_string()).collect();
.libraries
.iter()
.map(|s| s.to_string())
.collect();
ElfInfo { ElfInfo {
os_abi, os_abi,
@ -316,19 +261,12 @@ fn build_elf_info(elf: &Elf) -> ElfInfo {
} }
} }
fn collect_function_hints( fn collect_function_hints(elf: &Elf, entry_point: u64) -> Vec<u64> {
elf: &Elf,
entry_point: u64,
) -> Vec<u64> {
let mut hints: Vec<u64> = elf let mut hints: Vec<u64> = elf
.syms .syms
.iter() .iter()
.chain(elf.dynsyms.iter()) .chain(elf.dynsyms.iter())
.filter(|sym| { .filter(|sym| sym.st_type() == STT_FUNC && sym.st_value != 0 && sym.st_value != entry_point)
sym.st_type() == STT_FUNC
&& sym.st_value != 0
&& sym.st_value != entry_point
})
.map(|sym| sym.st_value) .map(|sym| sym.st_value)
.collect(); .collect();
hints.sort_unstable(); hints.sort_unstable();

View File

@ -26,21 +26,16 @@
// types.rs - Architecture, BinaryFormat, Endianness, // types.rs - Architecture, BinaryFormat, Endianness,
// SectionPermissions // SectionPermissions
use goblin::mach::cputype::{ use goblin::mach::cputype::{CPU_TYPE_ARM, CPU_TYPE_ARM64, CPU_TYPE_X86, CPU_TYPE_X86_64};
CPU_TYPE_ARM, CPU_TYPE_ARM64, CPU_TYPE_X86,
CPU_TYPE_X86_64,
};
use goblin::mach::load_command::CommandVariant; use goblin::mach::load_command::CommandVariant;
use goblin::mach::{Mach, MachO}; use goblin::mach::{Mach, MachO};
use super::{ use super::{
compute_section_hash, detect_common_anomalies, FormatResult, MachOInfo, SectionInfo, SegmentInfo, compute_section_hash,
FormatResult, MachOInfo, SectionInfo, SegmentInfo, detect_common_anomalies,
}; };
use crate::error::EngineError; use crate::error::EngineError;
use crate::types::{ use crate::types::{Architecture, BinaryFormat, Endianness, SectionPermissions};
Architecture, BinaryFormat, Endianness, SectionPermissions,
};
const MH_OBJECT: u32 = 1; const MH_OBJECT: u32 = 1;
const MH_EXECUTE: u32 = 2; const MH_EXECUTE: u32 = 2;
@ -53,34 +48,23 @@ const VM_PROT_READ: u32 = 0x01;
const VM_PROT_WRITE: u32 = 0x02; const VM_PROT_WRITE: u32 = 0x02;
const VM_PROT_EXECUTE: u32 = 0x04; const VM_PROT_EXECUTE: u32 = 0x04;
pub fn parse_macho( pub fn parse_macho(mach: &Mach, data: &[u8]) -> Result<FormatResult, EngineError> {
mach: &Mach,
data: &[u8],
) -> Result<FormatResult, EngineError> {
match mach { match mach {
Mach::Binary(macho) => { Mach::Binary(macho) => parse_single_macho(macho, data, false),
parse_single_macho(macho, data, false)
}
Mach::Fat(fat) => { Mach::Fat(fat) => {
for arch in fat.iter_arches() { for arch in fat.iter_arches() {
let arch = arch.map_err(|e| { let arch = arch.map_err(|e| EngineError::InvalidBinary {
EngineError::InvalidBinary { reason: e.to_string(),
reason: e.to_string(),
}
})?; })?;
let offset = arch.offset as usize; let offset = arch.offset as usize;
let size = arch.size as usize; let size = arch.size as usize;
let end = offset.saturating_add(size); let end = offset.saturating_add(size);
if end <= data.len() { if end <= data.len() {
let macho = MachO::parse(data, offset) let macho =
.map_err(|e| { MachO::parse(data, offset).map_err(|e| EngineError::InvalidBinary {
EngineError::InvalidBinary {
reason: e.to_string(), reason: e.to_string(),
} })?;
})?; return parse_single_macho(&macho, data, true);
return parse_single_macho(
&macho, data, true,
);
} }
} }
Err(EngineError::InvalidBinary { Err(EngineError::InvalidBinary {
@ -97,8 +81,7 @@ fn parse_single_macho(
data: &[u8], data: &[u8],
is_universal: bool, is_universal: bool,
) -> Result<FormatResult, EngineError> { ) -> Result<FormatResult, EngineError> {
let architecture = let architecture = map_architecture(macho.header.cputype);
map_architecture(macho.header.cputype);
let bits = if macho.is_64 { 64 } else { 32 }; let bits = if macho.is_64 { 64 } else { 32 };
let endianness = if macho.little_endian { let endianness = if macho.little_endian {
Endianness::Little Endianness::Little
@ -107,25 +90,20 @@ fn parse_single_macho(
}; };
let entry_point = macho.entry; let entry_point = macho.entry;
let symbols: Vec<_> = let symbols: Vec<_> = macho.symbols().flatten().collect();
macho.symbols().flatten().collect();
let is_stripped = symbols.is_empty(); let is_stripped = symbols.is_empty();
let has_debug_info = macho.segments.iter().any(|seg| { let has_debug_info = macho
seg.name().is_ok_and(|n| n == "__DWARF") .segments
}); .iter()
.any(|seg| seg.name().is_ok_and(|n| n == "__DWARF"));
let is_pie = macho.header.flags & 0x0020_0000 != 0; let is_pie = macho.header.flags & 0x0020_0000 != 0;
let sections = build_sections(macho, data); let sections = build_sections(macho, data);
let segments = build_segments(macho); let segments = build_segments(macho);
let anomalies = detect_common_anomalies( let anomalies = detect_common_anomalies(&sections, entry_point, is_stripped);
&sections, let macho_info = build_macho_info(macho, is_universal);
entry_point,
is_stripped,
);
let macho_info =
build_macho_info(macho, is_universal);
let function_hints: Vec<u64> = macho let function_hints: Vec<u64> = macho
.symbols() .symbols()
@ -164,11 +142,7 @@ fn map_architecture(cputype: u32) -> Architecture {
CPU_TYPE_X86_64 => Architecture::X86_64, CPU_TYPE_X86_64 => Architecture::X86_64,
CPU_TYPE_ARM => Architecture::Arm, CPU_TYPE_ARM => Architecture::Arm,
CPU_TYPE_ARM64 => Architecture::Aarch64, CPU_TYPE_ARM64 => Architecture::Aarch64,
other => { other => Architecture::Other(format!("mach-cpu-{other:#x}")),
Architecture::Other(format!(
"mach-cpu-{other:#x}"
))
}
} }
} }
@ -184,24 +158,19 @@ fn file_type_name(filetype: u32) -> String {
} }
} }
fn cpu_subtype_name( fn cpu_subtype_name(cputype: u32, cpusubtype: u32) -> String {
cputype: u32,
cpusubtype: u32,
) -> String {
let subtype = cpusubtype & 0x00FF_FFFF; let subtype = cpusubtype & 0x00FF_FFFF;
match cputype { match cputype {
CPU_TYPE_X86 | CPU_TYPE_X86_64 => { CPU_TYPE_X86 | CPU_TYPE_X86_64 => match subtype {
match subtype { 3 => "ALL".into(),
3 => "ALL".into(), 4 => "486".into(),
4 => "486".into(), 8 => "PENTIUM_3".into(),
8 => "PENTIUM_3".into(), 9 => "PENTIUM_M".into(),
9 => "PENTIUM_M".into(), 10 => "PENTIUM_4".into(),
10 => "PENTIUM_4".into(), 11 => "ITANIUM".into(),
11 => "ITANIUM".into(), 12 => "XEON".into(),
12 => "XEON".into(), _ => format!("{subtype}"),
_ => format!("{subtype}"), },
}
}
CPU_TYPE_ARM => match subtype { CPU_TYPE_ARM => match subtype {
6 => "v6".into(), 6 => "v6".into(),
9 => "v7".into(), 9 => "v7".into(),
@ -220,34 +189,23 @@ fn cpu_subtype_name(
} }
} }
fn build_sections( fn build_sections(macho: &MachO, data: &[u8]) -> Vec<SectionInfo> {
macho: &MachO,
data: &[u8],
) -> Vec<SectionInfo> {
let mut sections = Vec::new(); let mut sections = Vec::new();
for segment in macho.segments.iter() { for segment in macho.segments.iter() {
let initprot = segment.initprot; let initprot = segment.initprot;
let seg_permissions = SectionPermissions { let seg_permissions = SectionPermissions {
read: (initprot & VM_PROT_READ) != 0, read: (initprot & VM_PROT_READ) != 0,
write: (initprot & VM_PROT_WRITE) != 0, write: (initprot & VM_PROT_WRITE) != 0,
execute: (initprot & VM_PROT_EXECUTE) execute: (initprot & VM_PROT_EXECUTE) != 0,
!= 0,
}; };
for section_result in segment.into_iter() { for section_result in segment.into_iter() {
let Ok((section, _section_data)) = let Ok((section, _section_data)) = section_result else {
section_result
else {
continue; continue;
}; };
let name = section let name = section.name().unwrap_or("???").to_string();
.name()
.unwrap_or("???")
.to_string();
let raw_offset = section.offset as u64; let raw_offset = section.offset as u64;
let raw_size = section.size; let raw_size = section.size;
let sha256 = compute_section_hash( let sha256 = compute_section_hash(data, raw_offset, raw_size);
data, raw_offset, raw_size,
);
sections.push(SectionInfo { sections.push(SectionInfo {
name, name,
@ -268,16 +226,12 @@ fn build_segments(macho: &MachO) -> Vec<SegmentInfo> {
.segments .segments
.iter() .iter()
.map(|seg| { .map(|seg| {
let name = seg let name = seg.name().ok().map(|n| n.to_string());
.name()
.ok()
.map(|n| n.to_string());
let initprot = seg.initprot; let initprot = seg.initprot;
let permissions = SectionPermissions { let permissions = SectionPermissions {
read: (initprot & VM_PROT_READ) != 0, read: (initprot & VM_PROT_READ) != 0,
write: (initprot & VM_PROT_WRITE) != 0, write: (initprot & VM_PROT_WRITE) != 0,
execute: (initprot & VM_PROT_EXECUTE) execute: (initprot & VM_PROT_EXECUTE) != 0,
!= 0,
}; };
SegmentInfo { SegmentInfo {
@ -292,16 +246,9 @@ fn build_segments(macho: &MachO) -> Vec<SegmentInfo> {
.collect() .collect()
} }
fn build_macho_info( fn build_macho_info(macho: &MachO, is_universal: bool) -> MachOInfo {
macho: &MachO, let file_type = file_type_name(macho.header.filetype);
is_universal: bool, let cpu_subtype = cpu_subtype_name(macho.header.cputype, macho.header.cpusubtype);
) -> MachOInfo {
let file_type =
file_type_name(macho.header.filetype);
let cpu_subtype = cpu_subtype_name(
macho.header.cputype,
macho.header.cpusubtype,
);
let mut has_code_signature = false; let mut has_code_signature = false;
let mut has_function_starts = false; let mut has_function_starts = false;

View File

@ -35,9 +35,7 @@ use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use crate::error::EngineError; use crate::error::EngineError;
use crate::types::{ use crate::types::{Architecture, BinaryFormat, Endianness, SectionPermissions};
Architecture, BinaryFormat, Endianness, SectionPermissions,
};
pub const SUSPICIOUS_SECTION_NAMES: &[(&str, &str)] = &[ pub const SUSPICIOUS_SECTION_NAMES: &[(&str, &str)] = &[
("UPX0", "UPX packer"), ("UPX0", "UPX packer"),
@ -192,37 +190,22 @@ pub struct MachOInfo {
pub has_function_starts: bool, pub has_function_starts: bool,
} }
pub fn parse_format( pub fn parse_format(data: &[u8]) -> Result<FormatResult, EngineError> {
data: &[u8], let object = goblin::Object::parse(data).map_err(|e| EngineError::InvalidBinary {
) -> Result<FormatResult, EngineError> { reason: e.to_string(),
let object = })?;
goblin::Object::parse(data).map_err(|e| {
EngineError::InvalidBinary {
reason: e.to_string(),
}
})?;
match &object { match &object {
goblin::Object::Elf(elf_obj) => { goblin::Object::Elf(elf_obj) => elf::parse_elf(elf_obj, data),
elf::parse_elf(elf_obj, data) goblin::Object::PE(pe_obj) => pe::parse_pe(pe_obj, data),
} goblin::Object::Mach(mach_obj) => macho::parse_macho(mach_obj, data),
goblin::Object::PE(pe_obj) => {
pe::parse_pe(pe_obj, data)
}
goblin::Object::Mach(mach_obj) => {
macho::parse_macho(mach_obj, data)
}
_ => Err(EngineError::UnsupportedFormat { _ => Err(EngineError::UnsupportedFormat {
format: "unknown".into(), format: "unknown".into(),
}), }),
} }
} }
fn compute_section_hash( fn compute_section_hash(data: &[u8], offset: u64, size: u64) -> String {
data: &[u8],
offset: u64,
size: u64,
) -> String {
if size == 0 { if size == 0 {
return String::new(); return String::new();
} }
@ -251,53 +234,32 @@ fn detect_common_anomalies(
) -> Vec<FormatAnomaly> { ) -> Vec<FormatAnomaly> {
let mut anomalies = Vec::new(); let mut anomalies = Vec::new();
let text_section = let text_section = sections.iter().find(|s| s.name == ".text");
sections.iter().find(|s| s.name == ".text");
if let Some(text) = text_section { if let Some(text) = text_section {
let text_end = let text_end = text.virtual_address + text.virtual_size;
text.virtual_address + text.virtual_size; if entry_point != 0 && (entry_point < text.virtual_address || entry_point >= text_end) {
if entry_point != 0 anomalies.push(FormatAnomaly::EntryPointOutsideText {
&& (entry_point < text.virtual_address ep: entry_point,
|| entry_point >= text_end) text_range: (text.virtual_address, text_end),
{ });
anomalies.push(
FormatAnomaly::EntryPointOutsideText {
ep: entry_point,
text_range: (
text.virtual_address,
text_end,
),
},
);
} }
} }
if let Some(last) = sections.last() { if let Some(last) = sections.last() {
let last_end = let last_end = last.virtual_address + last.virtual_size;
last.virtual_address + last.virtual_size; if entry_point >= last.virtual_address && entry_point < last_end {
if entry_point >= last.virtual_address anomalies.push(FormatAnomaly::EntryPointInLastSection {
&& entry_point < last_end ep: entry_point,
{ section: last.name.clone(),
anomalies.push( });
FormatAnomaly::EntryPointInLastSection {
ep: entry_point,
section: last.name.clone(),
},
);
} }
} }
let ep_in_any = sections.iter().any(|s| { let ep_in_any = sections.iter().any(|s| {
entry_point >= s.virtual_address entry_point >= s.virtual_address && entry_point < s.virtual_address + s.virtual_size
&& entry_point
< s.virtual_address + s.virtual_size
}); });
if !ep_in_any && entry_point != 0 { if !ep_in_any && entry_point != 0 {
anomalies.push( anomalies.push(FormatAnomaly::EntryPointOutsideSections { ep: entry_point });
FormatAnomaly::EntryPointOutsideSections {
ep: entry_point,
},
);
} }
for (idx, section) in sections.iter().enumerate() { for (idx, section) in sections.iter().enumerate() {
@ -308,47 +270,31 @@ fn detect_common_anomalies(
} }
if section.name.is_empty() { if section.name.is_empty() {
anomalies.push( anomalies.push(FormatAnomaly::EmptySectionName { index: idx });
FormatAnomaly::EmptySectionName {
index: idx,
},
);
} }
if let Some(reason) = if let Some(reason) = check_suspicious_name(&section.name) {
check_suspicious_name(&section.name) anomalies.push(FormatAnomaly::SuspiciousSectionName {
{ name: section.name.clone(),
anomalies.push( reason,
FormatAnomaly::SuspiciousSectionName { });
name: section.name.clone(),
reason,
},
);
} }
if section.permissions.execute if section.permissions.execute && section.virtual_size == 0 {
&& section.virtual_size == 0 anomalies.push(FormatAnomaly::ZeroSizeCodeSection {
{ name: section.name.clone(),
anomalies.push( });
FormatAnomaly::ZeroSizeCodeSection {
name: section.name.clone(),
},
);
} }
if section.raw_size > 0 { if section.raw_size > 0 {
let ratio = section.virtual_size as f64 let ratio = section.virtual_size as f64 / section.raw_size as f64;
/ section.raw_size as f64;
if ratio > VIRTUAL_RAW_RATIO_THRESHOLD { if ratio > VIRTUAL_RAW_RATIO_THRESHOLD {
anomalies.push( anomalies.push(FormatAnomaly::VirtualRawSizeMismatch {
FormatAnomaly::VirtualRawSizeMismatch { name: section.name.clone(),
name: section.name.clone(), virtual_size: section.virtual_size,
virtual_size: section raw_size: section.raw_size,
.virtual_size, ratio,
raw_size: section.raw_size, });
ratio,
},
);
} }
} }
} }

View File

@ -28,14 +28,11 @@
use goblin::pe::PE; use goblin::pe::PE;
use super::{ use super::{
FormatAnomaly, FormatResult, PeDllCharacteristics, PeInfo, SectionInfo, SegmentInfo,
compute_section_hash, detect_common_anomalies, compute_section_hash, detect_common_anomalies,
FormatAnomaly, FormatResult, PeDllCharacteristics, PeInfo,
SectionInfo, SegmentInfo,
}; };
use crate::error::EngineError; use crate::error::EngineError;
use crate::types::{ use crate::types::{Architecture, BinaryFormat, Endianness, SectionPermissions};
Architecture, BinaryFormat, Endianness, SectionPermissions,
};
const COFF_MACHINE_I386: u16 = 0x14c; const COFF_MACHINE_I386: u16 = 0x14c;
const COFF_MACHINE_AMD64: u16 = 0x8664; const COFF_MACHINE_AMD64: u16 = 0x8664;
@ -48,8 +45,7 @@ const IMAGE_SCN_MEM_WRITE: u32 = 0x8000_0000;
const IMAGE_SCN_MEM_EXECUTE: u32 = 0x2000_0000; const IMAGE_SCN_MEM_EXECUTE: u32 = 0x2000_0000;
const IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE: u16 = 0x0040; const IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE: u16 = 0x0040;
const IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY: u16 = const IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY: u16 = 0x0080;
0x0080;
const IMAGE_DLLCHARACTERISTICS_NX_COMPAT: u16 = 0x0100; const IMAGE_DLLCHARACTERISTICS_NX_COMPAT: u16 = 0x0100;
const IMAGE_DLLCHARACTERISTICS_NO_SEH: u16 = 0x0400; const IMAGE_DLLCHARACTERISTICS_NO_SEH: u16 = 0x0400;
const IMAGE_DLLCHARACTERISTICS_GUARD_CF: u16 = 0x4000; const IMAGE_DLLCHARACTERISTICS_GUARD_CF: u16 = 0x4000;
@ -70,43 +66,30 @@ const PE_TIMESTAMP_MAX_VALID: u32 = 4_102_444_800;
const RICH_SIGNATURE: &[u8] = b"Rich"; const RICH_SIGNATURE: &[u8] = b"Rich";
pub fn parse_pe( pub fn parse_pe(pe: &PE, data: &[u8]) -> Result<FormatResult, EngineError> {
pe: &PE, let architecture = map_architecture(pe.header.coff_header.machine);
data: &[u8],
) -> Result<FormatResult, EngineError> {
let architecture = map_architecture(
pe.header.coff_header.machine,
);
let bits = if pe.is_64 { 64 } else { 32 }; let bits = if pe.is_64 { 64 } else { 32 };
let endianness = Endianness::Little; let endianness = Endianness::Little;
let entry_point = pe.entry as u64; let entry_point = pe.entry as u64;
let is_stripped = pe.debug_data.is_none(); let is_stripped = pe.debug_data.is_none();
let optional = pe.header.optional_header.as_ref(); let optional = pe.header.optional_header.as_ref();
let dll_chars = optional.map_or(0, |oh| { let dll_chars = optional.map_or(0, |oh| oh.windows_fields.dll_characteristics);
oh.windows_fields.dll_characteristics let is_pie = (dll_chars & IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE) != 0;
});
let is_pie = (dll_chars
& IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE)
!= 0;
let has_debug_info = pe.debug_data.is_some(); let has_debug_info = pe.debug_data.is_some();
let sections = build_sections(pe, data); let sections = build_sections(pe, data);
let segments = build_segments(pe); let segments = build_segments(pe);
let timestamp = let timestamp = pe.header.coff_header.time_date_stamp;
pe.header.coff_header.time_date_stamp; let image_base = optional.map_or(0, |oh| oh.windows_fields.image_base);
let image_base = optional let subsystem_raw = optional.map_or(0, |oh| oh.windows_fields.subsystem);
.map_or(0, |oh| oh.windows_fields.image_base);
let subsystem_raw = optional
.map_or(0, |oh| oh.windows_fields.subsystem);
let linker_version = optional.map_or_else( let linker_version = optional.map_or_else(
|| "0.0".into(), || "0.0".into(),
|oh| { |oh| {
format!( format!(
"{}.{}", "{}.{}",
oh.standard_fields.major_linker_version, oh.standard_fields.major_linker_version, oh.standard_fields.minor_linker_version,
oh.standard_fields.minor_linker_version,
) )
}, },
); );
@ -117,47 +100,29 @@ pub fn parse_pe(
let max_section_end = pe let max_section_end = pe
.sections .sections
.iter() .iter()
.map(|s| { .map(|s| s.pointer_to_raw_data as u64 + s.size_of_raw_data as u64)
s.pointer_to_raw_data as u64
+ s.size_of_raw_data as u64
})
.max() .max()
.unwrap_or(0); .unwrap_or(0);
let file_size = data.len() as u64; let file_size = data.len() as u64;
let has_overlay = let has_overlay = max_section_end > 0 && max_section_end < file_size;
max_section_end > 0 && max_section_end < file_size;
let overlay_size = if has_overlay { let overlay_size = if has_overlay {
file_size - max_section_end file_size - max_section_end
} else { } else {
0 0
}; };
let pe_offset = let pe_offset = pe.header.dos_header.pe_pointer as usize;
pe.header.dos_header.pe_pointer as usize; let rich_header_present = detect_rich_header(data, pe_offset);
let rich_header_present = detect_rich_header(
data,
pe_offset,
);
let pe_info = PeInfo { let pe_info = PeInfo {
image_base, image_base,
subsystem: subsystem_name(subsystem_raw), subsystem: subsystem_name(subsystem_raw),
dll_characteristics: PeDllCharacteristics { dll_characteristics: PeDllCharacteristics {
aslr: (dll_chars aslr: (dll_chars & IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE) != 0,
& IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE) dep: (dll_chars & IMAGE_DLLCHARACTERISTICS_NX_COMPAT) != 0,
!= 0, cfg: (dll_chars & IMAGE_DLLCHARACTERISTICS_GUARD_CF) != 0,
dep: (dll_chars no_seh: (dll_chars & IMAGE_DLLCHARACTERISTICS_NO_SEH) != 0,
& IMAGE_DLLCHARACTERISTICS_NX_COMPAT) force_integrity: (dll_chars & IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY) != 0,
!= 0,
cfg: (dll_chars
& IMAGE_DLLCHARACTERISTICS_GUARD_CF)
!= 0,
no_seh: (dll_chars
& IMAGE_DLLCHARACTERISTICS_NO_SEH)
!= 0,
force_integrity: (dll_chars
& IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY)
!= 0,
}, },
timestamp, timestamp,
linker_version, linker_version,
@ -167,11 +132,7 @@ pub fn parse_pe(
rich_header_present, rich_header_present,
}; };
let mut anomalies = detect_common_anomalies( let mut anomalies = detect_common_anomalies(&sections, entry_point, is_stripped);
&sections,
entry_point,
is_stripped,
);
detect_pe_anomalies( detect_pe_anomalies(
&mut anomalies, &mut anomalies,
pe, pe,
@ -213,15 +174,9 @@ fn map_architecture(machine: u16) -> Architecture {
match machine { match machine {
COFF_MACHINE_I386 => Architecture::X86, COFF_MACHINE_I386 => Architecture::X86,
COFF_MACHINE_AMD64 => Architecture::X86_64, COFF_MACHINE_AMD64 => Architecture::X86_64,
COFF_MACHINE_ARM | COFF_MACHINE_ARMNT => { COFF_MACHINE_ARM | COFF_MACHINE_ARMNT => Architecture::Arm,
Architecture::Arm
}
COFF_MACHINE_ARM64 => Architecture::Aarch64, COFF_MACHINE_ARM64 => Architecture::Aarch64,
other => { other => Architecture::Other(format!("pe-machine-{other:#x}")),
Architecture::Other(format!(
"pe-machine-{other:#x}"
))
}
} }
} }
@ -232,53 +187,33 @@ fn subsystem_name(subsystem: u16) -> String {
IMAGE_SUBSYSTEM_WINDOWS_GUI => "GUI".into(), IMAGE_SUBSYSTEM_WINDOWS_GUI => "GUI".into(),
IMAGE_SUBSYSTEM_WINDOWS_CUI => "Console".into(), IMAGE_SUBSYSTEM_WINDOWS_CUI => "Console".into(),
IMAGE_SUBSYSTEM_POSIX_CUI => "POSIX".into(), IMAGE_SUBSYSTEM_POSIX_CUI => "POSIX".into(),
IMAGE_SUBSYSTEM_WINDOWS_CE_GUI => { IMAGE_SUBSYSTEM_WINDOWS_CE_GUI => "Windows CE".into(),
"Windows CE".into() IMAGE_SUBSYSTEM_EFI_APPLICATION => "EFI Application".into(),
} IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER => "EFI Boot Service Driver".into(),
IMAGE_SUBSYSTEM_EFI_APPLICATION => { IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER => "EFI Runtime Driver".into(),
"EFI Application".into()
}
IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER => {
"EFI Boot Service Driver".into()
}
IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER => {
"EFI Runtime Driver".into()
}
IMAGE_SUBSYSTEM_XBOX => "Xbox".into(), IMAGE_SUBSYSTEM_XBOX => "Xbox".into(),
other => format!("Unknown({other})"), other => format!("Unknown({other})"),
} }
} }
fn build_sections( fn build_sections(pe: &PE, data: &[u8]) -> Vec<SectionInfo> {
pe: &PE,
data: &[u8],
) -> Vec<SectionInfo> {
pe.sections pe.sections
.iter() .iter()
.map(|section| { .map(|section| {
let name = section let name = section.name().unwrap_or_default().to_string();
.name() let raw_offset = section.pointer_to_raw_data as u64;
.unwrap_or_default() let raw_size = section.size_of_raw_data as u64;
.to_string();
let raw_offset =
section.pointer_to_raw_data as u64;
let raw_size =
section.size_of_raw_data as u64;
let chars = section.characteristics; let chars = section.characteristics;
let permissions = SectionPermissions { let permissions = SectionPermissions {
read: (chars & IMAGE_SCN_MEM_READ) != 0, read: (chars & IMAGE_SCN_MEM_READ) != 0,
write: (chars & IMAGE_SCN_MEM_WRITE) != 0, write: (chars & IMAGE_SCN_MEM_WRITE) != 0,
execute: (chars & IMAGE_SCN_MEM_EXECUTE) execute: (chars & IMAGE_SCN_MEM_EXECUTE) != 0,
!= 0,
}; };
let sha256 = compute_section_hash( let sha256 = compute_section_hash(data, raw_offset, raw_size);
data, raw_offset, raw_size,
);
SectionInfo { SectionInfo {
name, name,
virtual_address: section.virtual_address virtual_address: section.virtual_address as u64,
as u64,
virtual_size: section.virtual_size as u64, virtual_size: section.virtual_size as u64,
raw_offset, raw_offset,
raw_size, raw_size,
@ -293,10 +228,7 @@ fn build_segments(_pe: &PE) -> Vec<SegmentInfo> {
Vec::new() Vec::new()
} }
fn detect_rich_header( fn detect_rich_header(data: &[u8], pe_offset: usize) -> bool {
data: &[u8],
pe_offset: usize,
) -> bool {
let end = pe_offset.min(data.len()); let end = pe_offset.min(data.len());
data[..end] data[..end]
.windows(RICH_SIGNATURE.len()) .windows(RICH_SIGNATURE.len())
@ -313,34 +245,24 @@ fn detect_pe_anomalies(
file_size: u64, file_size: u64,
) { ) {
if timestamp == 0 { if timestamp == 0 {
anomalies.push( anomalies.push(FormatAnomaly::SuspiciousTimestamp {
FormatAnomaly::SuspiciousTimestamp { value: timestamp,
value: timestamp, reason: "zeroed timestamp".into(),
reason: "zeroed timestamp".into(), });
},
);
} else if timestamp < PE_TIMESTAMP_MIN_VALID { } else if timestamp < PE_TIMESTAMP_MIN_VALID {
anomalies.push( anomalies.push(FormatAnomaly::SuspiciousTimestamp {
FormatAnomaly::SuspiciousTimestamp { value: timestamp,
value: timestamp, reason: "timestamp before 1990".into(),
reason: "timestamp before 1990".into(), });
},
);
} else if timestamp > PE_TIMESTAMP_MAX_VALID { } else if timestamp > PE_TIMESTAMP_MAX_VALID {
anomalies.push( anomalies.push(FormatAnomaly::SuspiciousTimestamp {
FormatAnomaly::SuspiciousTimestamp { value: timestamp,
value: timestamp, reason: "timestamp after 2100".into(),
reason: "timestamp after 2100".into(), });
},
);
} }
if has_tls { if has_tls {
anomalies.push( anomalies.push(FormatAnomaly::TlsCallbacksPresent { count: 1 });
FormatAnomaly::TlsCallbacksPresent {
count: 1,
},
);
} }
if pe.imports.is_empty() { if pe.imports.is_empty() {

View File

@ -52,38 +52,30 @@ pub struct AnalysisEngine {
impl AnalysisEngine { impl AnalysisEngine {
pub fn new() -> Result<Self, EngineError> { pub fn new() -> Result<Self, EngineError> {
let passes: Vec<Box<dyn pass::AnalysisPass>> = let passes: Vec<Box<dyn pass::AnalysisPass>> = vec![
vec![ Box::new(FormatPass),
Box::new(FormatPass), Box::new(ImportPass),
Box::new(ImportPass), Box::new(StringPass),
Box::new(StringPass), Box::new(EntropyPass),
Box::new(EntropyPass), Box::new(DisasmPass),
Box::new(DisasmPass), Box::new(ThreatPass),
Box::new(ThreatPass), ];
];
let pass_manager = PassManager::new(passes); let pass_manager = PassManager::new(passes);
Ok(Self { pass_manager }) Ok(Self { pass_manager })
} }
pub fn analyze( pub fn analyze(&self, data: &[u8], file_name: &str) -> (AnalysisContext, PassReport) {
&self,
data: &[u8],
file_name: &str,
) -> (AnalysisContext, PassReport) {
let sha256 = compute_sha256(data); let sha256 = compute_sha256(data);
let file_size = data.len() as u64; let file_size = data.len() as u64;
let mut ctx = AnalysisContext::new( let mut ctx = AnalysisContext::new(
BinarySource::Buffered(Arc::from( BinarySource::Buffered(Arc::from(data.to_vec())),
data.to_vec(),
)),
sha256, sha256,
file_name.to_string(), file_name.to_string(),
file_size, file_size,
); );
let report = let report = self.pass_manager.run_all(&mut ctx);
self.pass_manager.run_all(&mut ctx);
(ctx, report) (ctx, report)
} }
} }

View File

@ -33,10 +33,7 @@ mod private {
pub trait AnalysisPass: private::Sealed + Send + Sync { pub trait AnalysisPass: private::Sealed + Send + Sync {
fn name(&self) -> &'static str; fn name(&self) -> &'static str;
fn dependencies(&self) -> &[&'static str]; fn dependencies(&self) -> &[&'static str];
fn run( fn run(&self, ctx: &mut AnalysisContext) -> Result<(), EngineError>;
&self,
ctx: &mut AnalysisContext,
) -> Result<(), EngineError>;
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -58,10 +55,7 @@ impl PassReport {
} }
pub fn failed_passes(&self) -> Vec<&PassOutcome> { pub fn failed_passes(&self) -> Vec<&PassOutcome> {
self.outcomes self.outcomes.iter().filter(|o| !o.success).collect()
.iter()
.filter(|o| !o.success)
.collect()
} }
} }
@ -71,33 +65,23 @@ pub struct PassManager {
} }
impl PassManager { impl PassManager {
pub fn new( pub fn new(passes: Vec<Box<dyn AnalysisPass>>) -> Self {
passes: Vec<Box<dyn AnalysisPass>>,
) -> Self {
let order = topological_order(&passes); let order = topological_order(&passes);
Self { passes, order } Self { passes, order }
} }
pub fn run_all( pub fn run_all(&self, ctx: &mut AnalysisContext) -> PassReport {
&self,
ctx: &mut AnalysisContext,
) -> PassReport {
let mut outcomes = Vec::with_capacity(self.passes.len()); let mut outcomes = Vec::with_capacity(self.passes.len());
for &idx in &self.order { for &idx in &self.order {
let pass = &self.passes[idx]; let pass = &self.passes[idx];
let start = Instant::now(); let start = Instant::now();
let result = pass.run(ctx); let result = pass.run(ctx);
let duration_ms = let duration_ms = start.elapsed().as_millis() as u64;
start.elapsed().as_millis() as u64;
let outcome = match result { let outcome = match result {
Ok(()) => { Ok(()) => {
tracing::info!( tracing::info!(pass = pass.name(), duration_ms, "pass completed");
pass = pass.name(),
duration_ms,
"pass completed"
);
PassOutcome { PassOutcome {
name: pass.name(), name: pass.name(),
success: true, success: true,
@ -128,9 +112,7 @@ impl PassManager {
} }
} }
fn topological_order( fn topological_order(passes: &[Box<dyn AnalysisPass>]) -> Vec<usize> {
passes: &[Box<dyn AnalysisPass>],
) -> Vec<usize> {
let name_to_idx: HashMap<&str, usize> = passes let name_to_idx: HashMap<&str, usize> = passes
.iter() .iter()
.enumerate() .enumerate()
@ -143,8 +125,7 @@ fn topological_order(
for (idx, pass) in passes.iter().enumerate() { for (idx, pass) in passes.iter().enumerate() {
for dep_name in pass.dependencies() { for dep_name in pass.dependencies() {
if let Some(&dep_idx) = name_to_idx.get(dep_name) if let Some(&dep_idx) = name_to_idx.get(dep_name) {
{
adjacency[dep_idx].push(idx); adjacency[dep_idx].push(idx);
in_degree[idx] += 1; in_degree[idx] += 1;
} }
@ -204,10 +185,7 @@ mod tests {
&self.deps &self.deps
} }
fn run( fn run(&self, _ctx: &mut AnalysisContext) -> Result<(), EngineError> {
&self,
_ctx: &mut AnalysisContext,
) -> Result<(), EngineError> {
self.log.lock().unwrap().push(self.name); self.log.lock().unwrap().push(self.name);
if self.should_fail { if self.should_fail {
return Err(EngineError::PassFailed { return Err(EngineError::PassFailed {
@ -221,9 +199,7 @@ mod tests {
fn make_ctx() -> AnalysisContext { fn make_ctx() -> AnalysisContext {
AnalysisContext::new( AnalysisContext::new(
crate::context::BinarySource::Buffered( crate::context::BinarySource::Buffered(Arc::from(vec![0u8; 4])),
Arc::from(vec![0u8; 4]),
),
"deadbeef".into(), "deadbeef".into(),
"test.bin".into(), "test.bin".into(),
4, 4,
@ -295,16 +271,10 @@ mod tests {
let report = manager.run_all(&mut ctx); let report = manager.run_all(&mut ctx);
let execution_order = log.lock().unwrap().clone(); let execution_order = log.lock().unwrap().clone();
assert_eq!( assert_eq!(execution_order, vec!["first", "second", "third"]);
execution_order,
vec!["first", "second", "third"]
);
assert!(!report.all_succeeded()); assert!(!report.all_succeeded());
assert_eq!(report.failed_passes().len(), 1); assert_eq!(report.failed_passes().len(), 1);
assert_eq!( assert_eq!(report.failed_passes()[0].name, "second");
report.failed_passes()[0].name,
"second"
);
} }
#[test] #[test]
@ -343,14 +313,10 @@ mod tests {
manager.run_all(&mut ctx); manager.run_all(&mut ctx);
let order = log.lock().unwrap().clone(); let order = log.lock().unwrap().clone();
let format_pos = let format_pos = order.iter().position(|&n| n == "format").unwrap();
order.iter().position(|&n| n == "format").unwrap(); let imports_pos = order.iter().position(|&n| n == "imports").unwrap();
let imports_pos = let entropy_pos = order.iter().position(|&n| n == "entropy").unwrap();
order.iter().position(|&n| n == "imports").unwrap(); let score_pos = order.iter().position(|&n| n == "score").unwrap();
let entropy_pos =
order.iter().position(|&n| n == "entropy").unwrap();
let score_pos =
order.iter().position(|&n| n == "score").unwrap();
assert!(format_pos < imports_pos); assert!(format_pos < imports_pos);
assert!(format_pos < entropy_pos); assert!(format_pos < entropy_pos);
@ -385,14 +351,12 @@ mod tests {
fn reports_duration() { fn reports_duration() {
let log = Arc::new(Mutex::new(Vec::new())); let log = Arc::new(Mutex::new(Vec::new()));
let passes: Vec<Box<dyn AnalysisPass>> = vec![ let passes: Vec<Box<dyn AnalysisPass>> = vec![Box::new(MockPass {
Box::new(MockPass { name: "fast",
name: "fast", deps: vec![],
deps: vec![], log: Arc::clone(&log),
log: Arc::clone(&log), should_fail: false,
should_fail: false, })];
}),
];
let manager = PassManager::new(passes); let manager = PassManager::new(passes);
let mut ctx = make_ctx(); let mut ctx = make_ctx();

View File

@ -41,23 +41,16 @@
// FlowControlType // FlowControlType
// error.rs - EngineError // error.rs - EngineError
use std::collections::{ use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
BTreeMap, HashMap, HashSet, VecDeque,
};
use iced_x86::{ use iced_x86::{Decoder, DecoderOptions, FlowControl, Formatter, Instruction, IntelFormatter};
Decoder, DecoderOptions, FlowControl, Formatter,
Instruction, IntelFormatter,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::context::AnalysisContext; use crate::context::AnalysisContext;
use crate::error::EngineError; use crate::error::EngineError;
use crate::formats::SectionInfo; use crate::formats::SectionInfo;
use crate::pass::{AnalysisPass, Sealed}; use crate::pass::{AnalysisPass, Sealed};
use crate::types::{ use crate::types::{Architecture, CfgEdgeType, FlowControlType};
Architecture, CfgEdgeType, FlowControlType,
};
const MAX_FUNCTIONS: usize = 1000; const MAX_FUNCTIONS: usize = 1000;
const MAX_INSTRUCTIONS: usize = 50_000; const MAX_INSTRUCTIONS: usize = 50_000;
@ -103,9 +96,7 @@ pub struct InstructionInfo {
pub flow_control: FlowControlType, pub flow_control: FlowControlType,
} }
#[derive( #[derive(Debug, Clone, Default, Serialize, Deserialize)]
Debug, Clone, Default, Serialize, Deserialize,
)]
pub struct FunctionCfg { pub struct FunctionCfg {
pub nodes: Vec<CfgNode>, pub nodes: Vec<CfgNode>,
pub edges: Vec<CfgEdge>, pub edges: Vec<CfgEdge>,
@ -139,17 +130,14 @@ impl AnalysisPass for DisasmPass {
&["format"] &["format"]
} }
fn run( fn run(&self, ctx: &mut AnalysisContext) -> Result<(), EngineError> {
&self, let format_result =
ctx: &mut AnalysisContext, ctx.format_result
) -> Result<(), EngineError> { .as_ref()
let format_result = ctx .ok_or_else(|| EngineError::MissingDependency {
.format_result pass: "disasm".into(),
.as_ref() dependency: "format".into(),
.ok_or_else(|| EngineError::MissingDependency { })?;
pass: "disasm".into(),
dependency: "format".into(),
})?;
let arch = &format_result.architecture; let arch = &format_result.architecture;
let bits = match arch { let bits = match arch {
@ -157,10 +145,7 @@ impl AnalysisPass for DisasmPass {
Architecture::X86_64 => 64, Architecture::X86_64 => 64,
_ => { _ => {
ctx.disassembly_result = ctx.disassembly_result =
Some(empty_result( Some(empty_result(format_result.bits, format_result.entry_point));
format_result.bits,
format_result.entry_point,
));
return Ok(()); return Ok(());
} }
}; };
@ -170,26 +155,15 @@ impl AnalysisPass for DisasmPass {
let entry_point = format_result.entry_point; let entry_point = format_result.entry_point;
let mut seeds = vec![entry_point]; let mut seeds = vec![entry_point];
seeds.extend_from_slice( seeds.extend_from_slice(&format_result.function_hints);
&format_result.function_hints,
);
let result = disassemble( let result = disassemble(data, sections, bits, entry_point, &seeds);
data,
sections,
bits,
entry_point,
&seeds,
);
ctx.disassembly_result = Some(result); ctx.disassembly_result = Some(result);
Ok(()) Ok(())
} }
} }
fn empty_result( fn empty_result(bits: u8, entry_point: u64) -> DisassemblyResult {
bits: u8,
entry_point: u64,
) -> DisassemblyResult {
DisassemblyResult { DisassemblyResult {
functions: Vec::new(), functions: Vec::new(),
total_instructions: 0, total_instructions: 0,
@ -214,34 +188,28 @@ fn disassemble(
let mut functions = Vec::new(); let mut functions = Vec::new();
let mut visited_functions = HashSet::new(); let mut visited_functions = HashSet::new();
let mut total_instructions = 0; let mut total_instructions = 0;
let mut function_queue: VecDeque<u64> = let mut function_queue: VecDeque<u64> = seeds.iter().copied().collect();
seeds.iter().copied().collect();
while let Some(func_addr) = function_queue.pop_front() while let Some(func_addr) = function_queue.pop_front() {
{ if functions.len() >= MAX_FUNCTIONS || total_instructions >= MAX_INSTRUCTIONS {
if functions.len() >= MAX_FUNCTIONS
|| total_instructions >= MAX_INSTRUCTIONS
{
break; break;
} }
if !visited_functions.insert(func_addr) { if !visited_functions.insert(func_addr) {
continue; continue;
} }
if vaddr_to_offset(sections, func_addr).is_none() if vaddr_to_offset(sections, func_addr).is_none() {
{
continue; continue;
} }
let (func_info, discovered_calls) = let (func_info, discovered_calls) = disassemble_function(
disassemble_function( data,
data, sections,
sections, &exec_sections,
&exec_sections, bits,
bits, func_addr,
func_addr, func_addr == entry_point,
func_addr == entry_point, MAX_INSTRUCTIONS - total_instructions,
MAX_INSTRUCTIONS - total_instructions, );
);
total_instructions += func_info.instruction_count; total_instructions += func_info.instruction_count;
functions.push(func_info); functions.push(func_info);
@ -273,8 +241,7 @@ fn disassemble_function(
is_entry_point: bool, is_entry_point: bool,
instruction_budget: usize, instruction_budget: usize,
) -> (FunctionInfo, Vec<u64>) { ) -> (FunctionInfo, Vec<u64>) {
let mut decoded: BTreeMap<u64, DecodedInstruction> = let mut decoded: BTreeMap<u64, DecodedInstruction> = BTreeMap::new();
BTreeMap::new();
let mut block_leaders: HashSet<u64> = HashSet::new(); let mut block_leaders: HashSet<u64> = HashSet::new();
let mut worklist: VecDeque<u64> = VecDeque::new(); let mut worklist: VecDeque<u64> = VecDeque::new();
let mut visited: HashSet<u64> = HashSet::new(); let mut visited: HashSet<u64> = HashSet::new();
@ -292,10 +259,7 @@ fn disassemble_function(
break; break;
} }
let offset = match vaddr_to_offset( let offset = match vaddr_to_offset(all_sections, addr) {
all_sections,
addr,
) {
Some(o) => o as usize, Some(o) => o as usize,
None => continue, None => continue,
}; };
@ -310,17 +274,10 @@ fn disassemble_function(
} }
let slice = &data[offset..]; let slice = &data[offset..];
let mut decoder = Decoder::with_ip( let mut decoder = Decoder::with_ip(bits, slice, addr, DecoderOptions::NONE);
bits,
slice,
addr,
DecoderOptions::NONE,
);
let mut instr = Instruction::default(); let mut instr = Instruction::default();
while decoder.can_decode() while decoder.can_decode() && decoded.len() < instruction_budget {
&& decoded.len() < instruction_budget
{
decoder.decode_out(&mut instr); decoder.decode_out(&mut instr);
let ip = instr.ip(); let ip = instr.ip();
@ -328,30 +285,21 @@ fn disassemble_function(
break; break;
} }
if ip != addr if ip != addr && block_leaders.contains(&ip) {
&& block_leaders.contains(&ip)
{
break; break;
} }
let fc = instr.flow_control(); let fc = instr.flow_control();
let mnemonic = format!("{:?}", instr.mnemonic()) let mnemonic = format!("{:?}", instr.mnemonic()).to_ascii_lowercase();
.to_ascii_lowercase();
let mut operands_str = String::new(); let mut operands_str = String::new();
formatter formatter.format(&instr, &mut operands_str);
.format(&instr, &mut operands_str);
let operands = operands_str let operands = operands_str
.split_once(' ') .split_once(' ')
.map_or(String::new(), |(_, ops)| { .map_or(String::new(), |(_, ops)| ops.to_string());
ops.to_string()
});
let instr_bytes = &data let instr_bytes =
[offset + (ip - addr) as usize &data[offset + (ip - addr) as usize..offset + (ip - addr) as usize + instr.len()];
..offset
+ (ip - addr) as usize
+ instr.len()];
let flow_type = map_flow_control(fc); let flow_type = map_flow_control(fc);
@ -374,13 +322,10 @@ fn disassemble_function(
match fc { match fc {
FlowControl::ConditionalBranch => { FlowControl::ConditionalBranch => {
let target = let target = instr.near_branch_target();
instr.near_branch_target();
let fall = instr.next_ip(); let fall = instr.next_ip();
if let Some(di) = if let Some(di) = decoded.get_mut(&ip) {
decoded.get_mut(&ip)
{
di.branch_target = Some(target); di.branch_target = Some(target);
di.fallthrough = Some(fall); di.fallthrough = Some(fall);
} }
@ -392,11 +337,8 @@ fn disassemble_function(
break; break;
} }
FlowControl::UnconditionalBranch => { FlowControl::UnconditionalBranch => {
let target = let target = instr.near_branch_target();
instr.near_branch_target(); if let Some(di) = decoded.get_mut(&ip) {
if let Some(di) =
decoded.get_mut(&ip)
{
di.branch_target = Some(target); di.branch_target = Some(target);
} }
block_leaders.insert(target); block_leaders.insert(target);
@ -410,45 +352,32 @@ fn disassemble_function(
break; break;
} }
FlowControl::Call => { FlowControl::Call => {
let target = let target = instr.near_branch_target();
instr.near_branch_target();
if target != 0 { if target != 0 {
discovered_calls.push(target); discovered_calls.push(target);
} }
} }
FlowControl::IndirectCall => {} FlowControl::IndirectCall => {}
FlowControl::Next FlowControl::Next | FlowControl::XbeginXabortXend => {}
| FlowControl::XbeginXabortXend => {}
} }
} }
} }
let basic_blocks = build_basic_blocks( let basic_blocks = build_basic_blocks(&decoded, &block_leaders);
&decoded, let instruction_count: usize = basic_blocks.iter().map(|bb| bb.instruction_count).sum();
&block_leaders,
);
let instruction_count: usize = basic_blocks
.iter()
.map(|bb| bb.instruction_count)
.sum();
let size = if let (Some(first), Some(last)) = ( let size =
decoded.keys().next(), if let (Some(first), Some(last)) = (decoded.keys().next(), decoded.keys().next_back()) {
decoded.keys().next_back(), if let Some(last_instr) = decoded.get(last) {
) { last_instr.info.address + last_instr.info.size as u64 - first
if let Some(last_instr) = decoded.get(last) { } else {
last_instr.info.address 0
+ last_instr.info.size as u64 }
- first
} else { } else {
0 0
} };
} else {
0
};
let cfg = if instruction_count <= CFG_INSTRUCTION_LIMIT let cfg = if instruction_count <= CFG_INSTRUCTION_LIMIT {
{
build_cfg(&basic_blocks) build_cfg(&basic_blocks)
} else { } else {
FunctionCfg::default() FunctionCfg::default()
@ -483,14 +412,11 @@ fn build_basic_blocks(
} }
let mut blocks: Vec<BasicBlockInfo> = Vec::new(); let mut blocks: Vec<BasicBlockInfo> = Vec::new();
let mut current_instrs: Vec<InstructionInfo> = let mut current_instrs: Vec<InstructionInfo> = Vec::new();
Vec::new();
let mut block_start: Option<u64> = None; let mut block_start: Option<u64> = None;
for (&addr, di) in decoded { for (&addr, di) in decoded {
if leaders.contains(&addr) if leaders.contains(&addr) && !current_instrs.is_empty() {
&& !current_instrs.is_empty()
{
let bb = finalize_block( let bb = finalize_block(
&current_instrs, &current_instrs,
block_start.unwrap_or(addr), block_start.unwrap_or(addr),
@ -527,35 +453,24 @@ fn build_basic_blocks(
} }
} }
if !current_instrs.is_empty() { if !current_instrs.is_empty()
if let Some(start) = block_start { && let Some(start) = block_start
let bb = finalize_block( {
&current_instrs, let bb = finalize_block(&current_instrs, start, decoded, leaders);
start, blocks.push(bb);
decoded,
leaders,
);
blocks.push(bb);
}
} }
let block_starts: HashSet<u64> = let block_starts: HashSet<u64> = blocks.iter().map(|b| b.start_address).collect();
blocks.iter().map(|b| b.start_address).collect();
for block in &mut blocks { for block in &mut blocks {
block block.successors.retain(|s| block_starts.contains(s));
.successors
.retain(|s| block_starts.contains(s));
} }
let predecessor_map: HashMap<u64, Vec<u64>> = { let predecessor_map: HashMap<u64, Vec<u64>> = {
let mut map: HashMap<u64, Vec<u64>> = let mut map: HashMap<u64, Vec<u64>> = HashMap::new();
HashMap::new();
for block in &blocks { for block in &blocks {
for &succ in &block.successors { for &succ in &block.successors {
map.entry(succ) map.entry(succ).or_default().push(block.start_address);
.or_default()
.push(block.start_address);
} }
} }
map map
@ -578,8 +493,7 @@ fn finalize_block(
leaders: &HashSet<u64>, leaders: &HashSet<u64>,
) -> BasicBlockInfo { ) -> BasicBlockInfo {
let last = instructions.last().unwrap(); let last = instructions.last().unwrap();
let end_address = let end_address = last.address + last.size as u64 - 1;
last.address + last.size as u64 - 1;
let mut successors = Vec::new(); let mut successors = Vec::new();
let last_addr = last.address; let last_addr = last.address;
@ -591,14 +505,10 @@ fn finalize_block(
successors.push(fall); successors.push(fall);
} else if !matches!( } else if !matches!(
di.info.flow_control, di.info.flow_control,
FlowControlType::Branch FlowControlType::Branch | FlowControlType::Return | FlowControlType::Interrupt
| FlowControlType::Return
| FlowControlType::Interrupt
) { ) {
let next = di.next_ip; let next = di.next_ip;
if leaders.contains(&next) if leaders.contains(&next) || decoded.contains_key(&next) {
|| decoded.contains_key(&next)
{
successors.push(next); successors.push(next);
} }
} }
@ -614,9 +524,7 @@ fn finalize_block(
} }
} }
fn build_cfg( fn build_cfg(blocks: &[BasicBlockInfo]) -> FunctionCfg {
blocks: &[BasicBlockInfo],
) -> FunctionCfg {
let mut nodes = Vec::new(); let mut nodes = Vec::new();
let mut edges = Vec::new(); let mut edges = Vec::new();
@ -635,40 +543,28 @@ fn build_cfg(
nodes.push(CfgNode { nodes.push(CfgNode {
id: block.start_address, id: block.start_address,
label: format!( label: format!("0x{:x}", block.start_address),
"0x{:x}",
block.start_address
),
instruction_count: block.instruction_count, instruction_count: block.instruction_count,
instructions_preview: preview, instructions_preview: preview,
}); });
let last_instr = block.instructions.last(); let last_instr = block.instructions.last();
for &succ in &block.successors { for &succ in &block.successors {
let edge_type = let edge_type = if let Some(last) = last_instr {
if let Some(last) = last_instr { match last.flow_control {
match last.flow_control { FlowControlType::ConditionalBranch => {
FlowControlType::ConditionalBranch => { if succ == block.successors.first().copied().unwrap_or(0) {
if succ CfgEdgeType::ConditionalTrue
== block } else {
.successors CfgEdgeType::ConditionalFalse
.first()
.copied()
.unwrap_or(0)
{
CfgEdgeType::ConditionalTrue
} else {
CfgEdgeType::ConditionalFalse
}
} }
FlowControlType::Branch => {
CfgEdgeType::Unconditional
}
_ => CfgEdgeType::Fallthrough,
} }
} else { FlowControlType::Branch => CfgEdgeType::Unconditional,
CfgEdgeType::Fallthrough _ => CfgEdgeType::Fallthrough,
}; }
} else {
CfgEdgeType::Fallthrough
};
edges.push(CfgEdge { edges.push(CfgEdge {
from: block.start_address, from: block.start_address,
@ -681,99 +577,54 @@ fn build_cfg(
FunctionCfg { nodes, edges } FunctionCfg { nodes, edges }
} }
fn vaddr_to_offset( fn vaddr_to_offset(sections: &[SectionInfo], vaddr: u64) -> Option<u64> {
sections: &[SectionInfo],
vaddr: u64,
) -> Option<u64> {
sections.iter().find_map(|s| { sections.iter().find_map(|s| {
if s.raw_size > 0 if s.raw_size > 0
&& vaddr >= s.virtual_address && vaddr >= s.virtual_address
&& vaddr && vaddr < s.virtual_address + s.virtual_size
< s.virtual_address + s.virtual_size
{ {
Some( Some(s.raw_offset + (vaddr - s.virtual_address))
s.raw_offset
+ (vaddr - s.virtual_address),
)
} else { } else {
None None
} }
}) })
} }
fn is_in_exec_section( fn is_in_exec_section(exec_sections: &[&SectionInfo], vaddr: u64) -> bool {
exec_sections: &[&SectionInfo], exec_sections
vaddr: u64, .iter()
) -> bool { .any(|s| vaddr >= s.virtual_address && vaddr < s.virtual_address + s.virtual_size)
exec_sections.iter().any(|s| {
vaddr >= s.virtual_address
&& vaddr
< s.virtual_address + s.virtual_size
})
} }
fn map_flow_control( fn map_flow_control(fc: FlowControl) -> FlowControlType {
fc: FlowControl,
) -> FlowControlType {
match fc { match fc {
FlowControl::Next FlowControl::Next | FlowControl::XbeginXabortXend => FlowControlType::Next,
| FlowControl::XbeginXabortXend => { FlowControl::UnconditionalBranch | FlowControl::IndirectBranch => FlowControlType::Branch,
FlowControlType::Next FlowControl::ConditionalBranch => FlowControlType::ConditionalBranch,
} FlowControl::Call | FlowControl::IndirectCall => FlowControlType::Call,
FlowControl::UnconditionalBranch
| FlowControl::IndirectBranch => {
FlowControlType::Branch
}
FlowControl::ConditionalBranch => {
FlowControlType::ConditionalBranch
}
FlowControl::Call
| FlowControl::IndirectCall => {
FlowControlType::Call
}
FlowControl::Return => FlowControlType::Return, FlowControl::Return => FlowControlType::Return,
FlowControl::Interrupt FlowControl::Interrupt | FlowControl::Exception => FlowControlType::Interrupt,
| FlowControl::Exception => {
FlowControlType::Interrupt
}
} }
} }
pub fn disassemble_code( pub fn disassemble_code(code: &[u8], base_addr: u64, bits: u32) -> Vec<InstructionInfo> {
code: &[u8], let mut decoder = Decoder::with_ip(bits, code, base_addr, DecoderOptions::NONE);
base_addr: u64,
bits: u32,
) -> Vec<InstructionInfo> {
let mut decoder = Decoder::with_ip(
bits,
code,
base_addr,
DecoderOptions::NONE,
);
let mut formatter = IntelFormatter::new(); let mut formatter = IntelFormatter::new();
let mut instr = Instruction::default(); let mut instr = Instruction::default();
let mut result = Vec::new(); let mut result = Vec::new();
while decoder.can_decode() { while decoder.can_decode() {
decoder.decode_out(&mut instr); decoder.decode_out(&mut instr);
let mnemonic = format!( let mnemonic = format!("{:?}", instr.mnemonic()).to_ascii_lowercase();
"{:?}",
instr.mnemonic()
)
.to_ascii_lowercase();
let mut full = String::new(); let mut full = String::new();
formatter.format(&instr, &mut full); formatter.format(&instr, &mut full);
let operands = full let operands = full
.split_once(' ') .split_once(' ')
.map_or(String::new(), |(_, ops)| { .map_or(String::new(), |(_, ops)| ops.to_string());
ops.to_string()
});
let start = let start = (instr.ip() - base_addr) as usize;
(instr.ip() - base_addr) as usize; let bytes = code[start..start + instr.len()].to_vec();
let bytes =
code[start..start + instr.len()].to_vec();
result.push(InstructionInfo { result.push(InstructionInfo {
address: instr.ip(), address: instr.ip(),
@ -781,9 +632,7 @@ pub fn disassemble_code(
mnemonic, mnemonic,
operands, operands,
size: instr.len() as u8, size: instr.len() as u8,
flow_control: map_flow_control( flow_control: map_flow_control(instr.flow_control()),
instr.flow_control(),
),
}); });
} }
@ -799,13 +648,8 @@ mod tests {
use crate::types::SectionPermissions; use crate::types::SectionPermissions;
fn load_fixture(name: &str) -> Vec<u8> { fn load_fixture(name: &str) -> Vec<u8> {
let path = format!( let path = format!("{}/tests/fixtures/{name}", env!("CARGO_MANIFEST_DIR"),);
"{}/tests/fixtures/{name}", std::fs::read(&path).unwrap_or_else(|e| panic!("fixture {path}: {e}"))
env!("CARGO_MANIFEST_DIR"),
);
std::fs::read(&path).unwrap_or_else(|e| {
panic!("fixture {path}: {e}")
})
} }
fn make_ctx(data: Vec<u8>) -> AnalysisContext { fn make_ctx(data: Vec<u8>) -> AnalysisContext {
@ -820,27 +664,17 @@ mod tests {
#[test] #[test]
fn disassemble_simple_function() { fn disassemble_simple_function() {
let code: &[u8] = &[ let code: &[u8] = &[0x55, 0x48, 0x89, 0xE5, 0x31, 0xC0, 0x5D, 0xC3];
0x55, 0x48, 0x89, 0xE5, 0x31, 0xC0, let instrs = disassemble_code(code, 0x1000, 64);
0x5D, 0xC3,
];
let instrs =
disassemble_code(code, 0x1000, 64);
assert_eq!(instrs.len(), 5); assert_eq!(instrs.len(), 5);
assert_eq!(instrs[0].mnemonic, "push"); assert_eq!(instrs[0].mnemonic, "push");
assert_eq!(instrs[4].mnemonic, "ret"); assert_eq!(instrs[4].mnemonic, "ret");
assert_eq!( assert_eq!(instrs[4].flow_control, FlowControlType::Return);
instrs[4].flow_control,
FlowControlType::Return
);
} }
#[test] #[test]
fn basic_block_split_on_branch() { fn basic_block_split_on_branch() {
let code: &[u8] = &[ let code: &[u8] = &[0x31, 0xC0, 0x85, 0xC0, 0x74, 0x02, 0x31, 0xC9, 0xC3];
0x31, 0xC0, 0x85, 0xC0, 0x74, 0x02,
0x31, 0xC9, 0xC3,
];
let sections = vec![SectionInfo { let sections = vec![SectionInfo {
name: ".text".into(), name: ".text".into(),
@ -856,13 +690,7 @@ mod tests {
sha256: String::new(), sha256: String::new(),
}]; }];
let result = disassemble( let result = disassemble(code, &sections, 64, 0x1000, &[0x1000]);
code,
&sections,
64,
0x1000,
&[0x1000],
);
assert!(!result.functions.is_empty()); assert!(!result.functions.is_empty());
let func = &result.functions[0]; let func = &result.functions[0];
assert!( assert!(
@ -874,10 +702,7 @@ mod tests {
#[test] #[test]
fn cfg_edges_conditional() { fn cfg_edges_conditional() {
let code: &[u8] = &[ let code: &[u8] = &[0x31, 0xC0, 0x85, 0xC0, 0x74, 0x02, 0x31, 0xC9, 0xC3];
0x31, 0xC0, 0x85, 0xC0, 0x74, 0x02,
0x31, 0xC9, 0xC3,
];
let sections = vec![SectionInfo { let sections = vec![SectionInfo {
name: ".text".into(), name: ".text".into(),
@ -893,47 +718,33 @@ mod tests {
sha256: String::new(), sha256: String::new(),
}]; }];
let result = disassemble( let result = disassemble(code, &sections, 64, 0x1000, &[0x1000]);
code,
&sections,
64,
0x1000,
&[0x1000],
);
let func = &result.functions[0]; let func = &result.functions[0];
assert!( assert!(!func.cfg.edges.is_empty(), "CFG should have edges");
!func.cfg.edges.is_empty(), assert!(!func.cfg.nodes.is_empty(), "CFG should have nodes");
"CFG should have edges"
);
assert!(
!func.cfg.nodes.is_empty(),
"CFG should have nodes"
);
} }
#[test] #[test]
fn non_x86_returns_empty() { fn non_x86_returns_empty() {
let data = vec![0u8; 64]; let data = vec![0u8; 64];
let mut ctx = make_ctx(data); let mut ctx = make_ctx(data);
ctx.format_result = ctx.format_result = Some(crate::formats::FormatResult {
Some(crate::formats::FormatResult { format: crate::types::BinaryFormat::Elf,
format: crate::types::BinaryFormat::Elf, architecture: Architecture::Aarch64,
architecture: Architecture::Aarch64, bits: 64,
bits: 64, endianness: crate::types::Endianness::Little,
endianness: entry_point: 0x1000,
crate::types::Endianness::Little, is_stripped: false,
entry_point: 0x1000, is_pie: false,
is_stripped: false, has_debug_info: false,
is_pie: false, sections: Vec::new(),
has_debug_info: false, segments: Vec::new(),
sections: Vec::new(), anomalies: Vec::new(),
segments: Vec::new(), pe_info: None,
anomalies: Vec::new(), elf_info: None,
pe_info: None, macho_info: None,
elf_info: None, function_hints: Vec::new(),
macho_info: None, });
function_hints: Vec::new(),
});
DisasmPass.run(&mut ctx).unwrap(); DisasmPass.run(&mut ctx).unwrap();
let result = ctx.disassembly_result.unwrap(); let result = ctx.disassembly_result.unwrap();
@ -946,28 +757,22 @@ mod tests {
let data = load_fixture("hello_elf"); let data = load_fixture("hello_elf");
let mut ctx = make_ctx(data); let mut ctx = make_ctx(data);
crate::passes::format::FormatPass crate::passes::format::FormatPass.run(&mut ctx).unwrap();
.run(&mut ctx)
.unwrap();
DisasmPass.run(&mut ctx).unwrap(); DisasmPass.run(&mut ctx).unwrap();
let result = let result = ctx.disassembly_result.as_ref().unwrap();
ctx.disassembly_result.as_ref().unwrap();
assert!( assert!(
result.total_functions > 0, result.total_functions > 0,
"should find at least one function" "should find at least one function"
); );
assert!(result.total_instructions > 0); assert!(result.total_instructions > 0);
let entry_func = result.functions.iter().find( let entry_func = result
|f| { .functions
f.address .iter()
== result.entry_function_address .find(|f| f.address == result.entry_function_address);
},
);
assert!( assert!(
entry_func.is_some() entry_func.is_some() || !result.functions.is_empty(),
|| !result.functions.is_empty(),
"should have disassembled functions" "should have disassembled functions"
); );
} }
@ -977,9 +782,7 @@ mod tests {
let data = load_fixture("hello_elf"); let data = load_fixture("hello_elf");
let mut ctx = make_ctx(data); let mut ctx = make_ctx(data);
crate::passes::format::FormatPass crate::passes::format::FormatPass.run(&mut ctx).unwrap();
.run(&mut ctx)
.unwrap();
assert!(ctx.disassembly_result.is_none()); assert!(ctx.disassembly_result.is_none());
DisasmPass.run(&mut ctx).unwrap(); DisasmPass.run(&mut ctx).unwrap();

View File

@ -42,9 +42,7 @@ use crate::context::AnalysisContext;
use crate::error::EngineError; use crate::error::EngineError;
use crate::formats::SectionInfo; use crate::formats::SectionInfo;
use crate::pass::{AnalysisPass, Sealed}; use crate::pass::{AnalysisPass, Sealed};
use crate::types::{ use crate::types::{EntropyClassification, EntropyFlag};
EntropyClassification, EntropyFlag,
};
const PLAINTEXT_MAX: f64 = 3.5; const PLAINTEXT_MAX: f64 = 3.5;
const NATIVE_CODE_MAX: f64 = 6.0; const NATIVE_CODE_MAX: f64 = 6.0;
@ -120,34 +118,23 @@ impl AnalysisPass for EntropyPass {
&["format"] &["format"]
} }
fn run( fn run(&self, ctx: &mut AnalysisContext) -> Result<(), EngineError> {
&self, let format_result =
ctx: &mut AnalysisContext, ctx.format_result
) -> Result<(), EngineError> { .as_ref()
let format_result = ctx .ok_or_else(|| EngineError::MissingDependency {
.format_result pass: "entropy".into(),
.as_ref() dependency: "format".into(),
.ok_or_else(|| EngineError::MissingDependency { })?;
pass: "entropy".into(),
dependency: "format".into(),
})?;
let data = ctx.data(); let data = ctx.data();
let result = analyze_entropy( let result = analyze_entropy(data, &format_result.sections, format_result.entry_point);
data,
&format_result.sections,
format_result.entry_point,
);
ctx.entropy_result = Some(result); ctx.entropy_result = Some(result);
Ok(()) Ok(())
} }
} }
fn analyze_entropy( fn analyze_entropy(data: &[u8], sections: &[SectionInfo], entry_point: u64) -> EntropyResult {
data: &[u8],
sections: &[SectionInfo],
entry_point: u64,
) -> EntropyResult {
let overall_entropy = shannon_entropy(data); let overall_entropy = shannon_entropy(data);
let mut section_entropies = Vec::new(); let mut section_entropies = Vec::new();
let mut packing_indicators = Vec::new(); let mut packing_indicators = Vec::new();
@ -155,11 +142,7 @@ fn analyze_entropy(
let mut structural_count = 0; let mut structural_count = 0;
for section in sections { for section in sections {
let section_data = read_section_data( let section_data = read_section_data(data, section.raw_offset, section.raw_size);
data,
section.raw_offset,
section.raw_size,
);
let entropy = if section_data.is_empty() { let entropy = if section_data.is_empty() {
0.0 0.0
} else { } else {
@ -167,8 +150,7 @@ fn analyze_entropy(
}; };
let classification = classify_entropy(entropy); let classification = classify_entropy(entropy);
let vr_ratio = if section.raw_size > 0 { let vr_ratio = if section.raw_size > 0 {
section.virtual_size as f64 section.virtual_size as f64 / section.raw_size as f64
/ section.raw_size as f64
} else { } else {
0.0 0.0
}; };
@ -181,24 +163,18 @@ fn analyze_entropy(
if vr_ratio > VIRTUAL_RAW_RATIO_THRESHOLD { if vr_ratio > VIRTUAL_RAW_RATIO_THRESHOLD {
flags.push(EntropyFlag::HighVirtualToRawRatio); flags.push(EntropyFlag::HighVirtualToRawRatio);
} }
if section.raw_size == 0 if section.raw_size == 0 && section.virtual_size > 0 {
&& section.virtual_size > 0
{
flags.push(EntropyFlag::EmptyRawData); flags.push(EntropyFlag::EmptyRawData);
} }
if section.permissions.is_rwx() { if section.permissions.is_rwx() {
flags.push(EntropyFlag::Rwx); flags.push(EntropyFlag::Rwx);
} }
if let Some(packer) = if let Some(packer) = detect_packer_by_section(&section.name) {
detect_packer_by_section(&section.name)
{
flags.push(EntropyFlag::PackerSectionName); flags.push(EntropyFlag::PackerSectionName);
packing_indicators.push(PackingIndicator { packing_indicators.push(PackingIndicator {
indicator_type: "section_name".into(), indicator_type: "section_name".into(),
description: format!( description: format!("Section name matches {packer} packer"),
"Section name matches {packer} packer"
),
evidence: section.name.clone(), evidence: section.name.clone(),
packer_name: Some(packer.into()), packer_name: Some(packer.into()),
}); });
@ -207,17 +183,13 @@ fn analyze_entropy(
} }
} }
if section.raw_size == 0 if section.raw_size == 0 && section.virtual_size > 0 && section.permissions.execute {
&& section.virtual_size > 0
&& section.permissions.execute
{
structural_count += 1; structural_count += 1;
packing_indicators.push(PackingIndicator { packing_indicators.push(PackingIndicator {
indicator_type: "structural".into(), indicator_type: "structural".into(),
description: description: "Empty raw data with executable \
"Empty raw data with executable \
virtual section" virtual section"
.into(), .into(),
evidence: format!( evidence: format!(
"section={} raw=0 virtual={}", "section={} raw=0 virtual={}",
section.name, section.virtual_size section.name, section.virtual_size
@ -230,13 +202,8 @@ fn analyze_entropy(
structural_count += 1; structural_count += 1;
packing_indicators.push(PackingIndicator { packing_indicators.push(PackingIndicator {
indicator_type: "structural".into(), indicator_type: "structural".into(),
description: description: "High virtual to raw size ratio".into(),
"High virtual to raw size ratio" evidence: format!("section={} ratio={vr_ratio:.1}", section.name),
.into(),
evidence: format!(
"section={} ratio={vr_ratio:.1}",
section.name
),
packer_name: None, packer_name: None,
}); });
} }
@ -254,38 +221,27 @@ fn analyze_entropy(
}); });
} }
if let Some(ep_section) = find_ep_section( if let Some(ep_section) = find_ep_section(sections, entry_point) {
sections,
entry_point,
) {
let ep_file_offset = entry_point let ep_file_offset = entry_point
.wrapping_sub(ep_section.virtual_address) .wrapping_sub(ep_section.virtual_address)
.wrapping_add(ep_section.raw_offset); .wrapping_add(ep_section.raw_offset);
if let Some(&first_byte) = if let Some(&first_byte) = data.get(ep_file_offset as usize)
data.get(ep_file_offset as usize) && first_byte == PUSHAD_OPCODE
{ {
if first_byte == PUSHAD_OPCODE { packing_indicators.push(PackingIndicator {
packing_indicators.push( indicator_type: "entry_point".into(),
PackingIndicator { description: "PUSHAD at entry point".into(),
indicator_type: "entry_point" evidence: format!(
.into(), "byte 0x{PUSHAD_OPCODE:02x} \
description:
"PUSHAD at entry point"
.into(),
evidence: format!(
"byte 0x{PUSHAD_OPCODE:02x} \
at EP offset 0x{ep_file_offset:x}" at EP offset 0x{ep_file_offset:x}"
), ),
packer_name: None, packer_name: None,
}, });
);
}
} }
} }
let packing_detected = packer_name.is_some() let packing_detected =
|| structural_count packer_name.is_some() || structural_count >= STRUCTURAL_INDICATORS_FOR_PACKING;
>= STRUCTURAL_INDICATORS_FOR_PACKING;
EntropyResult { EntropyResult {
overall_entropy, overall_entropy,
@ -314,9 +270,7 @@ fn shannon_entropy(data: &[u8]) -> f64 {
.sum() .sum()
} }
fn classify_entropy( fn classify_entropy(entropy: f64) -> EntropyClassification {
entropy: f64,
) -> EntropyClassification {
if entropy < PLAINTEXT_MAX { if entropy < PLAINTEXT_MAX {
EntropyClassification::Plaintext EntropyClassification::Plaintext
} else if entropy < NATIVE_CODE_MAX { } else if entropy < NATIVE_CODE_MAX {
@ -330,20 +284,14 @@ fn classify_entropy(
} }
} }
fn detect_packer_by_section( fn detect_packer_by_section(name: &str) -> Option<&'static str> {
name: &str,
) -> Option<&'static str> {
PACKER_SECTION_NAMES PACKER_SECTION_NAMES
.iter() .iter()
.find(|&&(section_name, _)| section_name == name) .find(|&&(section_name, _)| section_name == name)
.map(|&(_, packer)| packer) .map(|&(_, packer)| packer)
} }
fn read_section_data( fn read_section_data(data: &[u8], offset: u64, size: u64) -> &[u8] {
data: &[u8],
offset: u64,
size: u64,
) -> &[u8] {
if size == 0 { if size == 0 {
return &[]; return &[];
} }
@ -355,14 +303,9 @@ fn read_section_data(
&data[start..end] &data[start..end]
} }
fn find_ep_section( fn find_ep_section(sections: &[SectionInfo], entry_point: u64) -> Option<&SectionInfo> {
sections: &[SectionInfo],
entry_point: u64,
) -> Option<&SectionInfo> {
sections.iter().find(|s| { sections.iter().find(|s| {
entry_point >= s.virtual_address entry_point >= s.virtual_address && entry_point < s.virtual_address + s.virtual_size
&& entry_point
< s.virtual_address + s.virtual_size
}) })
} }
@ -375,13 +318,8 @@ mod tests {
use crate::types::SectionPermissions; use crate::types::SectionPermissions;
fn load_fixture(name: &str) -> Vec<u8> { fn load_fixture(name: &str) -> Vec<u8> {
let path = format!( let path = format!("{}/tests/fixtures/{name}", env!("CARGO_MANIFEST_DIR"),);
"{}/tests/fixtures/{name}", std::fs::read(&path).unwrap_or_else(|e| panic!("fixture {path}: {e}"))
env!("CARGO_MANIFEST_DIR"),
);
std::fs::read(&path).unwrap_or_else(|e| {
panic!("fixture {path}: {e}")
})
} }
fn make_ctx(data: Vec<u8>) -> AnalysisContext { fn make_ctx(data: Vec<u8>) -> AnalysisContext {
@ -402,8 +340,7 @@ mod tests {
#[test] #[test]
fn entropy_uniform_distribution() { fn entropy_uniform_distribution() {
let data: Vec<u8> = let data: Vec<u8> = (0..=255u8).cycle().take(1024).collect();
(0..=255u8).cycle().take(1024).collect();
let e = shannon_entropy(&data); let e = shannon_entropy(&data);
assert!( assert!(
(e - 8.0).abs() < 0.01, (e - 8.0).abs() < 0.01,
@ -421,50 +358,20 @@ mod tests {
#[test] #[test]
fn entropy_classification_thresholds() { fn entropy_classification_thresholds() {
assert_eq!( assert_eq!(classify_entropy(2.0), EntropyClassification::Plaintext);
classify_entropy(2.0), assert_eq!(classify_entropy(5.0), EntropyClassification::NativeCode);
EntropyClassification::Plaintext assert_eq!(classify_entropy(6.5), EntropyClassification::Compressed);
); assert_eq!(classify_entropy(7.1), EntropyClassification::Packed);
assert_eq!( assert_eq!(classify_entropy(7.5), EntropyClassification::Encrypted);
classify_entropy(5.0),
EntropyClassification::NativeCode
);
assert_eq!(
classify_entropy(6.5),
EntropyClassification::Compressed
);
assert_eq!(
classify_entropy(7.1),
EntropyClassification::Packed
);
assert_eq!(
classify_entropy(7.5),
EntropyClassification::Encrypted
);
} }
#[test] #[test]
fn packer_section_name_detection() { fn packer_section_name_detection() {
assert_eq!( assert_eq!(detect_packer_by_section("UPX0"), Some("UPX"));
detect_packer_by_section("UPX0"), assert_eq!(detect_packer_by_section("UPX1"), Some("UPX"));
Some("UPX") assert_eq!(detect_packer_by_section(".vmp0"), Some("VMProtect"));
); assert_eq!(detect_packer_by_section(".themida"), Some("Themida"));
assert_eq!( assert_eq!(detect_packer_by_section(".text"), None);
detect_packer_by_section("UPX1"),
Some("UPX")
);
assert_eq!(
detect_packer_by_section(".vmp0"),
Some("VMProtect")
);
assert_eq!(
detect_packer_by_section(".themida"),
Some("Themida")
);
assert_eq!(
detect_packer_by_section(".text"),
None
);
} }
#[test] #[test]
@ -483,17 +390,11 @@ mod tests {
sha256: String::new(), sha256: String::new(),
}]; }];
let data: Vec<u8> = let data: Vec<u8> = (0..=255u8).cycle().take(256).collect();
(0..=255u8).cycle().take(256).collect(); let result = analyze_entropy(&data, &sections, 0x1000);
let result =
analyze_entropy(&data, &sections, 0x1000);
let text_section = &result.sections[0]; let text_section = &result.sections[0];
assert!( assert!(text_section.entropy > HIGH_ENTROPY_THRESHOLD);
text_section.entropy > HIGH_ENTROPY_THRESHOLD assert!(text_section.flags.contains(&EntropyFlag::HighEntropy));
);
assert!(text_section
.flags
.contains(&EntropyFlag::HighEntropy));
assert!(text_section.is_anomalous); assert!(text_section.is_anomalous);
} }
@ -529,27 +430,17 @@ mod tests {
]; ];
let data = vec![0u8; 0x4200]; let data = vec![0u8; 0x4200];
let result = let result = analyze_entropy(&data, &sections, 0x11000);
analyze_entropy(&data, &sections, 0x11000);
assert!(result.packing_detected); assert!(result.packing_detected);
assert_eq!( assert_eq!(result.packer_name, Some("UPX".into()));
result.packer_name,
Some("UPX".into())
);
assert!(!result.packing_indicators.is_empty()); assert!(!result.packing_indicators.is_empty());
} }
#[test] #[test]
fn elf_entropy_analysis() { fn elf_entropy_analysis() {
let data = load_fixture("hello_elf"); let data = load_fixture("hello_elf");
let format_result = let format_result = crate::formats::parse_format(&data).unwrap();
crate::formats::parse_format(&data) let result = analyze_entropy(&data, &format_result.sections, format_result.entry_point);
.unwrap();
let result = analyze_entropy(
&data,
&format_result.sections,
format_result.entry_point,
);
assert!(result.overall_entropy > 0.0); assert!(result.overall_entropy > 0.0);
assert!(!result.sections.is_empty()); assert!(!result.sections.is_empty());
@ -561,9 +452,7 @@ mod tests {
let data = load_fixture("hello_elf"); let data = load_fixture("hello_elf");
let mut ctx = make_ctx(data); let mut ctx = make_ctx(data);
crate::passes::format::FormatPass crate::passes::format::FormatPass.run(&mut ctx).unwrap();
.run(&mut ctx)
.unwrap();
assert!(ctx.format_result.is_some()); assert!(ctx.format_result.is_some());
EntropyPass.run(&mut ctx).unwrap(); EntropyPass.run(&mut ctx).unwrap();

View File

@ -36,12 +36,8 @@ impl AnalysisPass for FormatPass {
&[] &[]
} }
fn run( fn run(&self, ctx: &mut AnalysisContext) -> Result<(), EngineError> {
&self, let result = formats::parse_format(ctx.data())?;
ctx: &mut AnalysisContext,
) -> Result<(), EngineError> {
let result =
formats::parse_format(ctx.data())?;
ctx.format_result = Some(result); ctx.format_result = Some(result);
Ok(()) Ok(())
} }
@ -56,13 +52,8 @@ mod tests {
use crate::types::{Architecture, BinaryFormat, Endianness}; use crate::types::{Architecture, BinaryFormat, Endianness};
fn load_fixture(name: &str) -> Vec<u8> { fn load_fixture(name: &str) -> Vec<u8> {
let path = format!( let path = format!("{}/tests/fixtures/{name}", env!("CARGO_MANIFEST_DIR"),);
"{}/tests/fixtures/{name}", std::fs::read(&path).unwrap_or_else(|e| panic!("fixture {path}: {e}"))
env!("CARGO_MANIFEST_DIR"),
);
std::fs::read(&path).unwrap_or_else(|e| {
panic!("fixture {path}: {e}")
})
} }
fn make_ctx(data: Vec<u8>) -> AnalysisContext { fn make_ctx(data: Vec<u8>) -> AnalysisContext {
@ -78,19 +69,12 @@ mod tests {
#[test] #[test]
fn parse_elf_basic_metadata() { fn parse_elf_basic_metadata() {
let data = load_fixture("hello_elf"); let data = load_fixture("hello_elf");
let result = let result = formats::parse_format(&data).unwrap();
formats::parse_format(&data).unwrap();
assert_eq!(result.format, BinaryFormat::Elf); assert_eq!(result.format, BinaryFormat::Elf);
assert_eq!( assert_eq!(result.architecture, Architecture::X86_64);
result.architecture,
Architecture::X86_64
);
assert_eq!(result.bits, 64); assert_eq!(result.bits, 64);
assert_eq!( assert_eq!(result.endianness, Endianness::Little);
result.endianness,
Endianness::Little
);
assert!(result.entry_point > 0); assert!(result.entry_point > 0);
assert!(result.is_pie); assert!(result.is_pie);
assert!(!result.is_stripped); assert!(!result.is_stripped);
@ -99,14 +83,10 @@ mod tests {
#[test] #[test]
fn parse_elf_sections_present() { fn parse_elf_sections_present() {
let data = load_fixture("hello_elf"); let data = load_fixture("hello_elf");
let result = let result = formats::parse_format(&data).unwrap();
formats::parse_format(&data).unwrap();
assert!(!result.sections.is_empty()); assert!(!result.sections.is_empty());
let text = result let text = result.sections.iter().find(|s| s.name == ".text");
.sections
.iter()
.find(|s| s.name == ".text");
assert!(text.is_some()); assert!(text.is_some());
assert!(text.unwrap().permissions.execute); assert!(text.unwrap().permissions.execute);
} }
@ -114,41 +94,35 @@ mod tests {
#[test] #[test]
fn parse_elf_segments_present() { fn parse_elf_segments_present() {
let data = load_fixture("hello_elf"); let data = load_fixture("hello_elf");
let result = let result = formats::parse_format(&data).unwrap();
formats::parse_format(&data).unwrap();
assert!(!result.segments.is_empty()); assert!(!result.segments.is_empty());
let load_segments: Vec<_> = result let load_segments: Vec<_> = result
.segments .segments
.iter() .iter()
.filter(|s| { .filter(|s| s.name.as_deref() == Some("LOAD"))
s.name.as_deref() == Some("LOAD")
})
.collect(); .collect();
assert!(!load_segments.is_empty()); assert!(!load_segments.is_empty());
} }
#[test] #[test]
fn parse_elf_stripped_detection() { fn parse_elf_stripped_detection() {
let data = let data = load_fixture("hello_elf_stripped");
load_fixture("hello_elf_stripped"); let result = formats::parse_format(&data).unwrap();
let result =
formats::parse_format(&data).unwrap();
assert!(result.is_stripped); assert!(result.is_stripped);
assert!(result.anomalies.iter().any(|a| { assert!(
matches!( result
a, .anomalies
FormatAnomaly::StrippedBinary .iter()
) .any(|a| { matches!(a, FormatAnomaly::StrippedBinary) })
})); );
} }
#[test] #[test]
fn parse_elf_info_populated() { fn parse_elf_info_populated() {
let data = load_fixture("hello_elf"); let data = load_fixture("hello_elf");
let result = let result = formats::parse_format(&data).unwrap();
formats::parse_format(&data).unwrap();
let elf_info = result.elf_info.unwrap(); let elf_info = result.elf_info.unwrap();
assert!(!elf_info.os_abi.is_empty()); assert!(!elf_info.os_abi.is_empty());
@ -160,18 +134,10 @@ mod tests {
#[test] #[test]
fn parse_elf_section_hashes() { fn parse_elf_section_hashes() {
let data = load_fixture("hello_elf"); let data = load_fixture("hello_elf");
let result = let result = formats::parse_format(&data).unwrap();
formats::parse_format(&data).unwrap();
let text = result let text = result.sections.iter().find(|s| s.name == ".text").unwrap();
.sections assert!(!text.sha256.is_empty(), ".text section should have a hash");
.iter()
.find(|s| s.name == ".text")
.unwrap();
assert!(
!text.sha256.is_empty(),
".text section should have a hash"
);
assert_eq!(text.sha256.len(), 64); assert_eq!(text.sha256.len(), 64);
} }
@ -184,8 +150,8 @@ mod tests {
#[test] #[test]
fn format_pass_populates_context() { fn format_pass_populates_context() {
use crate::pass::AnalysisPass;
use super::FormatPass; use super::FormatPass;
use crate::pass::AnalysisPass;
let data = load_fixture("hello_elf"); let data = load_fixture("hello_elf");
let mut ctx = make_ctx(data); let mut ctx = make_ctx(data);

View File

@ -47,6 +47,8 @@ use crate::error::EngineError;
use crate::pass::{AnalysisPass, Sealed}; use crate::pass::{AnalysisPass, Sealed};
use crate::types::Severity; use crate::types::Severity;
type ImportsExportsLibs = (Vec<ImportEntry>, Vec<ExportEntry>, Vec<String>);
pub struct SuspiciousApiDef { pub struct SuspiciousApiDef {
pub name: &'static str, pub name: &'static str,
pub tag: &'static str, pub tag: &'static str,
@ -179,11 +181,7 @@ const SUSPICIOUS_COMBINATIONS: &[CombinationDef] = &[
name: "Process Injection Chain", name: "Process Injection Chain",
description: "VirtualAllocEx + WriteProcessMemory \ description: "VirtualAllocEx + WriteProcessMemory \
+ CreateRemoteThread", + CreateRemoteThread",
patterns: &[ patterns: &["VirtualAllocEx", "WriteProcessMemory", "CreateRemoteThread"],
"VirtualAllocEx",
"WriteProcessMemory",
"CreateRemoteThread",
],
mitre_id: "T1055", mitre_id: "T1055",
severity: Severity::Critical, severity: Severity::Critical,
}, },
@ -211,50 +209,35 @@ const SUSPICIOUS_COMBINATIONS: &[CombinationDef] = &[
CombinationDef { CombinationDef {
name: "DLL Injection", name: "DLL Injection",
description: "LoadLibrary + CreateRemoteThread", description: "LoadLibrary + CreateRemoteThread",
patterns: &[ patterns: &["LoadLibrary*", "CreateRemoteThread"],
"LoadLibrary*",
"CreateRemoteThread",
],
mitre_id: "T1055.001", mitre_id: "T1055.001",
severity: Severity::High, severity: Severity::High,
}, },
CombinationDef { CombinationDef {
name: "Credential Theft", name: "Credential Theft",
description: "OpenProcess + ReadProcessMemory", description: "OpenProcess + ReadProcessMemory",
patterns: &[ patterns: &["OpenProcess", "ReadProcessMemory"],
"OpenProcess",
"ReadProcessMemory",
],
mitre_id: "T1003", mitre_id: "T1003",
severity: Severity::Critical, severity: Severity::Critical,
}, },
CombinationDef { CombinationDef {
name: "Service Persistence", name: "Service Persistence",
description: "OpenSCManager + CreateService", description: "OpenSCManager + CreateService",
patterns: &[ patterns: &["OpenSCManager*", "CreateService*"],
"OpenSCManager*",
"CreateService*",
],
mitre_id: "T1543.003", mitre_id: "T1543.003",
severity: Severity::Medium, severity: Severity::Medium,
}, },
CombinationDef { CombinationDef {
name: "Registry Persistence", name: "Registry Persistence",
description: "RegOpenKeyEx + RegSetValueEx", description: "RegOpenKeyEx + RegSetValueEx",
patterns: &[ patterns: &["RegOpenKeyEx*", "RegSetValueEx*"],
"RegOpenKeyEx*",
"RegSetValueEx*",
],
mitre_id: "T1547.001", mitre_id: "T1547.001",
severity: Severity::Medium, severity: Severity::Medium,
}, },
CombinationDef { CombinationDef {
name: "Download and Execute", name: "Download and Execute",
description: "URLDownloadToFile + ShellExecute", description: "URLDownloadToFile + ShellExecute",
patterns: &[ patterns: &["URLDownloadToFile*", "ShellExecute*"],
"URLDownloadToFile*",
"ShellExecute*",
],
mitre_id: "T1105", mitre_id: "T1105",
severity: Severity::High, severity: Severity::High,
}, },
@ -283,11 +266,7 @@ const SUSPICIOUS_COMBINATIONS: &[CombinationDef] = &[
name: "Linux C2 Connection", name: "Linux C2 Connection",
description: "socket + connect + inet_pton \ description: "socket + connect + inet_pton \
hardcoded C2 address", hardcoded C2 address",
patterns: &[ patterns: &["socket", "connect", "inet_pton"],
"socket",
"connect",
"inet_pton",
],
mitre_id: "T1071", mitre_id: "T1071",
severity: Severity::High, severity: Severity::High,
}, },
@ -295,12 +274,7 @@ const SUSPICIOUS_COMBINATIONS: &[CombinationDef] = &[
name: "Linux Network Listener", name: "Linux Network Listener",
description: "socket + bind + listen + accept \ description: "socket + bind + listen + accept \
backdoor listener", backdoor listener",
patterns: &[ patterns: &["socket", "bind", "listen", "accept"],
"socket",
"bind",
"listen",
"accept",
],
mitre_id: "T1571", mitre_id: "T1571",
severity: Severity::High, severity: Severity::High,
}, },
@ -327,8 +301,7 @@ pub struct ImportResult {
pub imports: Vec<ImportEntry>, pub imports: Vec<ImportEntry>,
pub exports: Vec<ExportEntry>, pub exports: Vec<ExportEntry>,
pub libraries: Vec<String>, pub libraries: Vec<String>,
pub suspicious_combinations: pub suspicious_combinations: Vec<SuspiciousCombination>,
Vec<SuspiciousCombination>,
pub mitre_mappings: Vec<MitreMapping>, pub mitre_mappings: Vec<MitreMapping>,
pub statistics: ImportStatistics, pub statistics: ImportStatistics,
} }
@ -389,43 +362,28 @@ impl AnalysisPass for ImportPass {
&["format"] &["format"]
} }
fn run( fn run(&self, ctx: &mut AnalysisContext) -> Result<(), EngineError> {
&self,
ctx: &mut AnalysisContext,
) -> Result<(), EngineError> {
let result = analyze_imports(ctx.data())?; let result = analyze_imports(ctx.data())?;
ctx.import_result = Some(result); ctx.import_result = Some(result);
Ok(()) Ok(())
} }
} }
fn analyze_imports( fn analyze_imports(data: &[u8]) -> Result<ImportResult, EngineError> {
data: &[u8], let object = goblin::Object::parse(data).map_err(|e| EngineError::InvalidBinary {
) -> Result<ImportResult, EngineError> { reason: e.to_string(),
let object = })?;
goblin::Object::parse(data).map_err(|e| {
EngineError::InvalidBinary {
reason: e.to_string(),
}
})?;
let (imports, exports, libraries) = match &object { let (imports, exports, libraries) = match &object {
goblin::Object::Elf(elf) => extract_elf(elf), goblin::Object::Elf(elf) => extract_elf(elf),
goblin::Object::PE(pe) => extract_pe(pe), goblin::Object::PE(pe) => extract_pe(pe),
goblin::Object::Mach(mach) => { goblin::Object::Mach(mach) => extract_mach(mach, data)?,
extract_mach(mach, data)?
}
_ => (Vec::new(), Vec::new(), Vec::new()), _ => (Vec::new(), Vec::new(), Vec::new()),
}; };
let suspicious_combinations = let suspicious_combinations = detect_combinations(&imports);
detect_combinations(&imports); let mitre_mappings = collect_mitre_mappings(&imports);
let mitre_mappings = let suspicious_count = imports.iter().filter(|i| i.is_suspicious).count();
collect_mitre_mappings(&imports);
let suspicious_count = imports
.iter()
.filter(|i| i.is_suspicious)
.count();
let statistics = ImportStatistics { let statistics = ImportStatistics {
total_imports: imports.len(), total_imports: imports.len(),
@ -444,29 +402,19 @@ fn analyze_imports(
}) })
} }
fn extract_elf( fn extract_elf(elf: &goblin::elf::Elf) -> ImportsExportsLibs {
elf: &goblin::elf::Elf, let libraries: Vec<String> = elf.libraries.iter().map(|s| s.to_string()).collect();
) -> (Vec<ImportEntry>, Vec<ExportEntry>, Vec<String>) {
let libraries: Vec<String> = elf
.libraries
.iter()
.map(|s| s.to_string())
.collect();
let mut imports = Vec::new(); let mut imports = Vec::new();
for sym in elf.dynsyms.iter() { for sym in elf.dynsyms.iter() {
if !sym.is_import() || sym.st_name == 0 { if !sym.is_import() || sym.st_name == 0 {
continue; continue;
} }
let name = elf let name = elf.dynstrtab.get_at(sym.st_name).unwrap_or("");
.dynstrtab
.get_at(sym.st_name)
.unwrap_or("");
if name.is_empty() { if name.is_empty() {
continue; continue;
} }
let (is_suspicious, threat_tags) = let (is_suspicious, threat_tags) = flag_suspicious(name);
flag_suspicious(name);
imports.push(ImportEntry { imports.push(ImportEntry {
library: String::new(), library: String::new(),
function: name.to_string(), function: name.to_string(),
@ -479,16 +427,10 @@ fn extract_elf(
let mut exports = Vec::new(); let mut exports = Vec::new();
for sym in elf.dynsyms.iter() { for sym in elf.dynsyms.iter() {
if sym.is_import() if sym.is_import() || sym.st_value == 0 || sym.st_name == 0 {
|| sym.st_value == 0
|| sym.st_name == 0
{
continue; continue;
} }
let name = elf let name = elf.dynstrtab.get_at(sym.st_name).unwrap_or("");
.dynstrtab
.get_at(sym.st_name)
.unwrap_or("");
if name.is_empty() { if name.is_empty() {
continue; continue;
} }
@ -504,17 +446,14 @@ fn extract_elf(
(imports, exports, libraries) (imports, exports, libraries)
} }
fn extract_pe( fn extract_pe(pe: &goblin::pe::PE) -> ImportsExportsLibs {
pe: &goblin::pe::PE,
) -> (Vec<ImportEntry>, Vec<ExportEntry>, Vec<String>) {
let mut lib_set = HashSet::new(); let mut lib_set = HashSet::new();
let mut imports = Vec::new(); let mut imports = Vec::new();
for import in &pe.imports { for import in &pe.imports {
let dll = import.dll.to_string(); let dll = import.dll.to_string();
lib_set.insert(dll.clone()); lib_set.insert(dll.clone());
let (is_suspicious, threat_tags) = let (is_suspicious, threat_tags) = flag_suspicious(&import.name);
flag_suspicious(&import.name);
imports.push(ImportEntry { imports.push(ImportEntry {
library: dll, library: dll,
function: import.name.to_string(), function: import.name.to_string(),
@ -528,17 +467,12 @@ fn extract_pe(
let mut exports = Vec::new(); let mut exports = Vec::new();
for export in &pe.exports { for export in &pe.exports {
let is_forwarded = export.reexport.is_some(); let is_forwarded = export.reexport.is_some();
let forward_target = let forward_target = export.reexport.as_ref().map(|r| match r {
export.reexport.as_ref().map(|r| match r { goblin::pe::export::Reexport::DLLName { export: name, lib } => format!("{lib}!{name}"),
goblin::pe::export::Reexport::DLLName { goblin::pe::export::Reexport::DLLOrdinal { ordinal, lib } => {
export: name, format!("{lib}!#{ordinal}")
lib, }
} => format!("{lib}!{name}"), });
goblin::pe::export::Reexport::DLLOrdinal {
ordinal,
lib,
} => format!("{lib}!#{ordinal}"),
});
exports.push(ExportEntry { exports.push(ExportEntry {
name: export.name.map(|s| s.to_string()), name: export.name.map(|s| s.to_string()),
address: export.rva as u64, address: export.rva as u64,
@ -548,38 +482,24 @@ fn extract_pe(
}); });
} }
let libraries: Vec<String> = let libraries: Vec<String> = lib_set.into_iter().collect();
lib_set.into_iter().collect();
(imports, exports, libraries) (imports, exports, libraries)
} }
fn extract_mach( fn extract_mach(mach: &goblin::mach::Mach, data: &[u8]) -> Result<ImportsExportsLibs, EngineError> {
mach: &goblin::mach::Mach,
data: &[u8],
) -> Result<
(Vec<ImportEntry>, Vec<ExportEntry>, Vec<String>),
EngineError,
> {
match mach { match mach {
goblin::mach::Mach::Binary(macho) => { goblin::mach::Mach::Binary(macho) => Ok(extract_single_macho(macho)),
Ok(extract_single_macho(macho))
}
goblin::mach::Mach::Fat(fat) => { goblin::mach::Mach::Fat(fat) => {
for arch in fat.iter_arches() { if let Some(arch) = fat.iter_arches().next() {
let arch = arch.map_err(|e| { let arch = arch.map_err(|e| EngineError::InvalidBinary {
EngineError::InvalidBinary { reason: e.to_string(),
reason: e.to_string(),
}
})?;
let macho = goblin::mach::MachO::parse(
data,
arch.offset as usize,
)
.map_err(|e| {
EngineError::InvalidBinary {
reason: e.to_string(),
}
})?; })?;
let macho =
goblin::mach::MachO::parse(data, arch.offset as usize).map_err(|e| {
EngineError::InvalidBinary {
reason: e.to_string(),
}
})?;
return Ok(extract_single_macho(&macho)); return Ok(extract_single_macho(&macho));
} }
Ok((Vec::new(), Vec::new(), Vec::new())) Ok((Vec::new(), Vec::new(), Vec::new()))
@ -587,14 +507,11 @@ fn extract_mach(
} }
} }
fn extract_single_macho( fn extract_single_macho(macho: &goblin::mach::MachO) -> ImportsExportsLibs {
macho: &goblin::mach::MachO,
) -> (Vec<ImportEntry>, Vec<ExportEntry>, Vec<String>) {
let mut imports = Vec::new(); let mut imports = Vec::new();
if let Ok(macho_imports) = macho.imports() { if let Ok(macho_imports) = macho.imports() {
for imp in &macho_imports { for imp in &macho_imports {
let (is_suspicious, threat_tags) = let (is_suspicious, threat_tags) = flag_suspicious(imp.name);
flag_suspicious(imp.name);
imports.push(ImportEntry { imports.push(ImportEntry {
library: imp.dylib.to_string(), library: imp.dylib.to_string(),
function: imp.name.to_string(), function: imp.name.to_string(),
@ -629,9 +546,7 @@ fn extract_single_macho(
(imports, exports, libraries) (imports, exports, libraries)
} }
fn flag_suspicious( fn flag_suspicious(name: &str) -> (bool, Vec<String>) {
name: &str,
) -> (bool, Vec<String>) {
let mut tags = Vec::new(); let mut tags = Vec::new();
for api in SUSPICIOUS_APIS { for api in SUSPICIOUS_APIS {
if matches_api(name, api.name) { if matches_api(name, api.name) {
@ -642,24 +557,17 @@ fn flag_suspicious(
(is_suspicious, tags) (is_suspicious, tags)
} }
fn matches_api( fn matches_api(import_name: &str, api_name: &str) -> bool {
import_name: &str,
api_name: &str,
) -> bool {
if import_name == api_name { if import_name == api_name {
return true; return true;
} }
if import_name.starts_with(api_name) { if let Some(suffix) = import_name.strip_prefix(api_name) {
let suffix = &import_name[api_name.len()..];
return suffix == "A" || suffix == "W"; return suffix == "A" || suffix == "W";
} }
false false
} }
fn matches_pattern( fn matches_pattern(import_name: &str, pattern: &str) -> bool {
import_name: &str,
pattern: &str,
) -> bool {
if let Some(prefix) = pattern.strip_suffix('*') { if let Some(prefix) = pattern.strip_suffix('*') {
import_name.starts_with(prefix) import_name.starts_with(prefix)
} else { } else {
@ -667,13 +575,8 @@ fn matches_pattern(
} }
} }
fn detect_combinations( fn detect_combinations(imports: &[ImportEntry]) -> Vec<SuspiciousCombination> {
imports: &[ImportEntry], let function_names: Vec<&str> = imports.iter().map(|i| i.function.as_str()).collect();
) -> Vec<SuspiciousCombination> {
let function_names: Vec<&str> = imports
.iter()
.map(|i| i.function.as_str())
.collect();
let mut results = Vec::new(); let mut results = Vec::new();
let mut seen = HashSet::new(); let mut seen = HashSet::new();
@ -681,12 +584,11 @@ fn detect_combinations(
if seen.contains(combo.name) { if seen.contains(combo.name) {
continue; continue;
} }
let all_matched = let all_matched = combo.patterns.iter().all(|pattern| {
combo.patterns.iter().all(|pattern| { function_names
function_names.iter().any(|name| { .iter()
matches_pattern(name, pattern) .any(|name| matches_pattern(name, pattern))
}) });
});
if !all_matched { if !all_matched {
continue; continue;
} }
@ -697,9 +599,7 @@ fn detect_combinations(
.filter_map(|pattern| { .filter_map(|pattern| {
function_names function_names
.iter() .iter()
.find(|name| { .find(|name| matches_pattern(name, pattern))
matches_pattern(name, pattern)
})
.map(|name| name.to_string()) .map(|name| name.to_string())
}) })
.collect(); .collect();
@ -717,18 +617,14 @@ fn detect_combinations(
results results
} }
fn collect_mitre_mappings( fn collect_mitre_mappings(imports: &[ImportEntry]) -> Vec<MitreMapping> {
imports: &[ImportEntry],
) -> Vec<MitreMapping> {
let mut mappings = Vec::new(); let mut mappings = Vec::new();
for import in imports { for import in imports {
if !import.is_suspicious { if !import.is_suspicious {
continue; continue;
} }
for api in SUSPICIOUS_APIS { for api in SUSPICIOUS_APIS {
if matches_api(&import.function, api.name) if matches_api(&import.function, api.name) && !api.mitre_id.is_empty() {
&& !api.mitre_id.is_empty()
{
mappings.push(MitreMapping { mappings.push(MitreMapping {
technique_id: api.mitre_id.into(), technique_id: api.mitre_id.into(),
api: import.function.clone(), api: import.function.clone(),
@ -748,13 +644,8 @@ mod tests {
use crate::context::BinarySource; use crate::context::BinarySource;
fn load_fixture(name: &str) -> Vec<u8> { fn load_fixture(name: &str) -> Vec<u8> {
let path = format!( let path = format!("{}/tests/fixtures/{name}", env!("CARGO_MANIFEST_DIR"),);
"{}/tests/fixtures/{name}", std::fs::read(&path).unwrap_or_else(|e| panic!("fixture {path}: {e}"))
env!("CARGO_MANIFEST_DIR"),
);
std::fs::read(&path).unwrap_or_else(|e| {
panic!("fixture {path}: {e}")
})
} }
fn make_ctx(data: Vec<u8>) -> AnalysisContext { fn make_ctx(data: Vec<u8>) -> AnalysisContext {
@ -770,13 +661,9 @@ mod tests {
#[test] #[test]
fn elf_imports_extracted() { fn elf_imports_extracted() {
let data = load_fixture("hello_elf"); let data = load_fixture("hello_elf");
let result = let result = analyze_imports(&data).unwrap();
analyze_imports(&data).unwrap();
assert!( assert!(!result.imports.is_empty(), "ELF binary should have imports");
!result.imports.is_empty(),
"ELF binary should have imports"
);
assert!( assert!(
!result.libraries.is_empty(), !result.libraries.is_empty(),
"ELF binary should list needed libraries" "ELF binary should list needed libraries"
@ -786,20 +673,15 @@ mod tests {
#[test] #[test]
fn suspicious_api_flagging() { fn suspicious_api_flagging() {
let (is_suspicious, tags) = let (is_suspicious, tags) = flag_suspicious("VirtualAllocEx");
flag_suspicious("VirtualAllocEx");
assert!(is_suspicious); assert!(is_suspicious);
assert!(tags.contains(&"injection".to_string())); assert!(tags.contains(&"injection".to_string()));
let (is_suspicious, tags) = let (is_suspicious, tags) = flag_suspicious("RegSetValueExW");
flag_suspicious("RegSetValueExW");
assert!(is_suspicious); assert!(is_suspicious);
assert!(tags.contains( assert!(tags.contains(&"persistence".to_string()));
&"persistence".to_string()
));
let (is_suspicious, _) = let (is_suspicious, _) = flag_suspicious("printf");
flag_suspicious("printf");
assert!(!is_suspicious); assert!(!is_suspicious);
} }
@ -812,9 +694,7 @@ mod tests {
address: None, address: None,
ordinal: None, ordinal: None,
is_suspicious: true, is_suspicious: true,
threat_tags: vec![ threat_tags: vec!["injection".into()],
"injection".into(),
],
}, },
ImportEntry { ImportEntry {
library: "kernel32.dll".into(), library: "kernel32.dll".into(),
@ -822,9 +702,7 @@ mod tests {
address: None, address: None,
ordinal: None, ordinal: None,
is_suspicious: true, is_suspicious: true,
threat_tags: vec![ threat_tags: vec!["injection".into()],
"injection".into(),
],
}, },
ImportEntry { ImportEntry {
library: "kernel32.dll".into(), library: "kernel32.dll".into(),
@ -832,23 +710,15 @@ mod tests {
address: None, address: None,
ordinal: None, ordinal: None,
is_suspicious: true, is_suspicious: true,
threat_tags: vec![ threat_tags: vec!["injection".into()],
"injection".into(),
],
}, },
]; ];
let combos = detect_combinations(&imports); let combos = detect_combinations(&imports);
assert_eq!(combos.len(), 1); assert_eq!(combos.len(), 1);
assert_eq!( assert_eq!(combos[0].name, "Process Injection Chain");
combos[0].name,
"Process Injection Chain"
);
assert_eq!(combos[0].mitre_id, "T1055"); assert_eq!(combos[0].mitre_id, "T1055");
assert_eq!( assert_eq!(combos[0].severity, Severity::Critical);
combos[0].severity,
Severity::Critical
);
} }
#[test] #[test]
@ -868,17 +738,12 @@ mod tests {
address: None, address: None,
ordinal: None, ordinal: None,
is_suspicious: true, is_suspicious: true,
threat_tags: vec![ threat_tags: vec!["persistence".into()],
"persistence".into(),
],
}, },
]; ];
let combos = detect_combinations(&imports); let combos = detect_combinations(&imports);
assert!(combos assert!(combos.iter().any(|c| c.name == "Registry Persistence"));
.iter()
.any(|c| c.name
== "Registry Persistence"));
} }
#[test] #[test]
@ -894,8 +759,7 @@ mod tests {
let combos = detect_combinations(&imports); let combos = detect_combinations(&imports);
assert!( assert!(
!combos.iter().any(|c| c.name !combos.iter().any(|c| c.name == "Process Injection Chain"),
== "Process Injection Chain"),
"should not detect chain with only one API" "should not detect chain with only one API"
); );
} }
@ -909,9 +773,7 @@ mod tests {
address: None, address: None,
ordinal: None, ordinal: None,
is_suspicious: true, is_suspicious: true,
threat_tags: vec![ threat_tags: vec!["injection".into()],
"injection".into(),
],
}, },
ImportEntry { ImportEntry {
library: String::new(), library: String::new(),
@ -923,13 +785,9 @@ mod tests {
}, },
]; ];
let mappings = let mappings = collect_mitre_mappings(&imports);
collect_mitre_mappings(&imports);
assert_eq!(mappings.len(), 1); assert_eq!(mappings.len(), 1);
assert_eq!( assert_eq!(mappings[0].technique_id, "T1055.008");
mappings[0].technique_id,
"T1055.008"
);
assert_eq!(mappings[0].api, "ptrace"); assert_eq!(mappings[0].api, "ptrace");
} }

View File

@ -70,23 +70,14 @@ const SUSPICIOUS_CATEGORIES: &[StringCategory] = &[
StringCategory::CryptoWallet, StringCategory::CryptoWallet,
]; ];
const URL_PREFIXES: &[&str] = const URL_PREFIXES: &[&str] = &["http://", "https://", "ftp://"];
&["http://", "https://", "ftp://"];
const UNIX_PATH_PREFIXES: &[&str] = &[ const UNIX_PATH_PREFIXES: &[&str] = &[
"/etc/", "/tmp/", "/var/", "/bin/", "/usr/", "/etc/", "/tmp/", "/var/", "/bin/", "/usr/", "/dev/", "/proc/", "/sys/", "/opt/", "/home/",
"/dev/", "/proc/", "/sys/", "/opt/", "/home/",
"/root/", "/lib/", "/sbin/", "/root/", "/lib/", "/sbin/",
]; ];
const REGISTRY_PREFIXES: &[&str] = &[ const REGISTRY_PREFIXES: &[&str] = &["HKEY_", "HKLM\\", "HKCU\\", "HKCR\\", "HKCC\\", "HKU\\"];
"HKEY_",
"HKLM\\",
"HKCU\\",
"HKCR\\",
"HKCC\\",
"HKU\\",
];
const SHELL_INDICATORS: &[&str] = &[ const SHELL_INDICATORS: &[&str] = &[
"cmd.exe", "cmd.exe",
@ -108,8 +99,7 @@ const SHELL_INDICATORS: &[&str] = &[
]; ];
const PACKER_SIGNATURES: &[&str] = &[ const PACKER_SIGNATURES: &[&str] = &[
"UPX!", "MPRESS", ".themida", ".vmp", ".enigma", "UPX!", "MPRESS", ".themida", ".vmp", ".enigma", "PEC2", "ASPack", "MEW ",
"PEC2", "ASPack", "MEW ",
]; ];
const DEBUG_ARTIFACTS: &[&str] = &[ const DEBUG_ARTIFACTS: &[&str] = &[
@ -163,8 +153,7 @@ const PERSISTENCE_PATHS: &[&str] = &[
".config/autostart", ".config/autostart",
]; ];
const BASE64_CHARS: &[u8] = const BASE64_CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const BASE64_MIN_LENGTH: usize = 20; const BASE64_MIN_LENGTH: usize = 20;
@ -215,25 +204,15 @@ impl AnalysisPass for StringPass {
&["format"] &["format"]
} }
fn run( fn run(&self, ctx: &mut AnalysisContext) -> Result<(), EngineError> {
&self, let sections = ctx.format_result.as_ref().map(|fr| fr.sections.as_slice());
ctx: &mut AnalysisContext, let result = extract_strings(ctx.data(), sections);
) -> Result<(), EngineError> {
let sections = ctx
.format_result
.as_ref()
.map(|fr| fr.sections.as_slice());
let result =
extract_strings(ctx.data(), sections);
ctx.string_result = Some(result); ctx.string_result = Some(result);
Ok(()) Ok(())
} }
} }
fn extract_strings( fn extract_strings(data: &[u8], sections: Option<&[SectionInfo]>) -> StringResult {
data: &[u8],
sections: Option<&[SectionInfo]>,
) -> StringResult {
let mut strings = Vec::new(); let mut strings = Vec::new();
extract_ascii(data, sections, &mut strings); extract_ascii(data, sections, &mut strings);
@ -244,12 +223,8 @@ fn extract_strings(
let mut suspicious_count = 0; let mut suspicious_count = 0;
for s in &strings { for s in &strings {
*by_encoding *by_encoding.entry(s.encoding.clone()).or_insert(0) += 1;
.entry(s.encoding.clone()) *by_category.entry(s.category.clone()).or_insert(0) += 1;
.or_insert(0) += 1;
*by_category
.entry(s.category.clone())
.or_insert(0) += 1;
if s.is_suspicious { if s.is_suspicious {
suspicious_count += 1; suspicious_count += 1;
} }
@ -270,17 +245,10 @@ fn extract_strings(
} }
fn is_printable(b: u8) -> bool { fn is_printable(b: u8) -> bool {
(PRINTABLE_MIN..=PRINTABLE_MAX).contains(&b) (PRINTABLE_MIN..=PRINTABLE_MAX).contains(&b) || b == TAB || b == NEWLINE || b == CARRIAGE_RETURN
|| b == TAB
|| b == NEWLINE
|| b == CARRIAGE_RETURN
} }
fn extract_ascii( fn extract_ascii(data: &[u8], sections: Option<&[SectionInfo]>, out: &mut Vec<ExtractedString>) {
data: &[u8],
sections: Option<&[SectionInfo]>,
out: &mut Vec<ExtractedString>,
) {
let mut start = 0; let mut start = 0;
let mut in_run = false; let mut in_run = false;
@ -292,61 +260,18 @@ fn extract_ascii(
} }
} else if in_run { } else if in_run {
let len = i - start; let len = i - start;
if len >= MIN_STRING_LENGTH { if len >= MIN_STRING_LENGTH
if let Ok(s) = && let Ok(s) = std::str::from_utf8(&data[start..i])
std::str::from_utf8(&data[start..i])
{
let is_multibyte = s
.bytes()
.any(|b| !b.is_ascii());
let encoding = if is_multibyte {
StringEncoding::Utf8
} else {
StringEncoding::Ascii
};
let category = classify(s);
let is_suspicious =
is_category_suspicious(&category);
let section = sections.and_then(|secs| {
find_section(
secs,
start as u64,
)
});
out.push(ExtractedString {
value: s.to_string(),
offset: start as u64,
encoding,
length: len,
category,
is_suspicious,
section,
});
}
}
in_run = false;
}
}
if in_run {
let len = data.len() - start;
if len >= MIN_STRING_LENGTH {
if let Ok(s) =
std::str::from_utf8(&data[start..])
{ {
let is_multibyte = let is_multibyte = s.bytes().any(|b| !b.is_ascii());
s.bytes().any(|b| !b.is_ascii());
let encoding = if is_multibyte { let encoding = if is_multibyte {
StringEncoding::Utf8 StringEncoding::Utf8
} else { } else {
StringEncoding::Ascii StringEncoding::Ascii
}; };
let category = classify(s); let category = classify(s);
let is_suspicious = let is_suspicious = is_category_suspicious(&category);
is_category_suspicious(&category); let section = sections.and_then(|secs| find_section(secs, start as u64));
let section = sections.and_then(|secs| {
find_section(secs, start as u64)
});
out.push(ExtractedString { out.push(ExtractedString {
value: s.to_string(), value: s.to_string(),
offset: start as u64, offset: start as u64,
@ -357,15 +282,38 @@ fn extract_ascii(
section, section,
}); });
} }
in_run = false;
}
}
if in_run {
let len = data.len() - start;
if len >= MIN_STRING_LENGTH
&& let Ok(s) = std::str::from_utf8(&data[start..])
{
let is_multibyte = s.bytes().any(|b| !b.is_ascii());
let encoding = if is_multibyte {
StringEncoding::Utf8
} else {
StringEncoding::Ascii
};
let category = classify(s);
let is_suspicious = is_category_suspicious(&category);
let section = sections.and_then(|secs| find_section(secs, start as u64));
out.push(ExtractedString {
value: s.to_string(),
offset: start as u64,
encoding,
length: len,
category,
is_suspicious,
section,
});
} }
} }
} }
fn extract_utf16le( fn extract_utf16le(data: &[u8], sections: Option<&[SectionInfo]>, out: &mut Vec<ExtractedString>) {
data: &[u8],
sections: Option<&[SectionInfo]>,
out: &mut Vec<ExtractedString>,
) {
let mut i = 0; let mut i = 0;
while i + 1 < data.len() { while i + 1 < data.len() {
let lo = data[i]; let lo = data[i];
@ -391,14 +339,10 @@ fn extract_utf16le(
} }
if code_units.len() >= MIN_STRING_LENGTH { if code_units.len() >= MIN_STRING_LENGTH {
let value = let value = String::from_utf16_lossy(&code_units);
String::from_utf16_lossy(&code_units);
let category = classify(&value); let category = classify(&value);
let is_suspicious = let is_suspicious = is_category_suspicious(&category);
is_category_suspicious(&category); let section = sections.and_then(|secs| find_section(secs, start as u64));
let section = sections.and_then(|secs| {
find_section(secs, start as u64)
});
out.push(ExtractedString { out.push(ExtractedString {
value, value,
offset: start as u64, offset: start as u64,
@ -417,17 +361,11 @@ fn extract_utf16le(
} }
} }
fn find_section( fn find_section(sections: &[SectionInfo], file_offset: u64) -> Option<String> {
sections: &[SectionInfo],
file_offset: u64,
) -> Option<String> {
sections sections
.iter() .iter()
.find(|s| { .find(|s| {
s.raw_size > 0 s.raw_size > 0 && file_offset >= s.raw_offset && file_offset < s.raw_offset + s.raw_size
&& file_offset >= s.raw_offset
&& file_offset
< s.raw_offset + s.raw_size
}) })
.map(|s| s.name.clone()) .map(|s| s.name.clone())
} }
@ -475,17 +413,13 @@ fn classify(s: &str) -> StringCategory {
StringCategory::Generic StringCategory::Generic
} }
fn is_category_suspicious( fn is_category_suspicious(category: &StringCategory) -> bool {
category: &StringCategory,
) -> bool {
SUSPICIOUS_CATEGORIES.contains(category) SUSPICIOUS_CATEGORIES.contains(category)
} }
fn is_url(s: &str) -> bool { fn is_url(s: &str) -> bool {
let lower = s.to_ascii_lowercase(); let lower = s.to_ascii_lowercase();
URL_PREFIXES URL_PREFIXES.iter().any(|prefix| lower.starts_with(prefix))
.iter()
.any(|prefix| lower.starts_with(prefix))
} }
fn is_ip_address(s: &str) -> bool { fn is_ip_address(s: &str) -> bool {
@ -521,9 +455,7 @@ fn is_file_path(s: &str) -> bool {
} }
fn is_registry_key(s: &str) -> bool { fn is_registry_key(s: &str) -> bool {
REGISTRY_PREFIXES REGISTRY_PREFIXES.iter().any(|prefix| s.starts_with(prefix))
.iter()
.any(|prefix| s.starts_with(prefix))
} }
fn is_shell_command(s: &str) -> bool { fn is_shell_command(s: &str) -> bool {
@ -537,11 +469,10 @@ fn is_crypto_wallet(s: &str) -> bool {
if s.len() >= BTC_MIN_LENGTH if s.len() >= BTC_MIN_LENGTH
&& s.len() <= BTC_MAX_LENGTH && s.len() <= BTC_MAX_LENGTH
&& (s.starts_with('1') || s.starts_with('3')) && (s.starts_with('1') || s.starts_with('3'))
&& s.chars() && s.chars().all(|c| c.is_ascii_alphanumeric())
.all(|c| c.is_ascii_alphanumeric()) && !s
&& !s.chars().any(|c| { .chars()
c == '0' || c == 'O' || c == 'I' || c == 'l' .any(|c| c == '0' || c == 'O' || c == 'I' || c == 'l')
})
{ {
return true; return true;
} }
@ -572,51 +503,35 @@ fn is_email(s: &str) -> bool {
return false; return false;
} }
let local = &s[..at_pos]; let local = &s[..at_pos];
local local.chars().all(|c| {
c.is_ascii_alphanumeric() || c == '.' || c == '_' || c == '%' || c == '+' || c == '-'
}) && domain[..dot_pos]
.chars() .chars()
.all(|c| c.is_ascii_alphanumeric() .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-')
|| c == '.'
|| c == '_'
|| c == '%'
|| c == '+'
|| c == '-')
&& domain[..dot_pos]
.chars()
.all(|c| c.is_ascii_alphanumeric()
|| c == '.'
|| c == '-')
&& tld.chars().all(|c| c.is_ascii_alphabetic()) && tld.chars().all(|c| c.is_ascii_alphabetic())
} }
fn is_suspicious_api(s: &str) -> bool { fn is_suspicious_api(s: &str) -> bool {
SUSPICIOUS_APIS SUSPICIOUS_APIS.iter().any(|api| s == api.name)
.iter()
.any(|api| s == api.name)
} }
fn is_packer_signature(s: &str) -> bool { fn is_packer_signature(s: &str) -> bool {
PACKER_SIGNATURES PACKER_SIGNATURES.iter().any(|sig| s.contains(sig))
.iter()
.any(|sig| s.contains(sig))
} }
fn is_debug_artifact(s: &str) -> bool { fn is_debug_artifact(s: &str) -> bool {
DEBUG_ARTIFACTS DEBUG_ARTIFACTS.iter().any(|artifact| s.contains(artifact))
.iter()
.any(|artifact| s.contains(artifact))
} }
fn is_anti_analysis(s: &str) -> bool { fn is_anti_analysis(s: &str) -> bool {
let lower = s.to_ascii_lowercase(); let lower = s.to_ascii_lowercase();
ANTI_ANALYSIS_INDICATORS.iter().any(|indicator| { ANTI_ANALYSIS_INDICATORS
lower.contains(&indicator.to_ascii_lowercase()) .iter()
}) .any(|indicator| lower.contains(&indicator.to_ascii_lowercase()))
} }
fn is_persistence_path(s: &str) -> bool { fn is_persistence_path(s: &str) -> bool {
PERSISTENCE_PATHS PERSISTENCE_PATHS.iter().any(|path| s.contains(path))
.iter()
.any(|path| s.contains(path))
} }
fn is_encoded_data(s: &str) -> bool { fn is_encoded_data(s: &str) -> bool {
@ -627,9 +542,7 @@ fn is_encoded_data(s: &str) -> bool {
if trimmed.is_empty() { if trimmed.is_empty() {
return false; return false;
} }
let all_base64 = trimmed let all_base64 = trimmed.bytes().all(|b| BASE64_CHARS.contains(&b));
.bytes()
.all(|b| BASE64_CHARS.contains(&b));
if !all_base64 { if !all_base64 {
return false; return false;
} }
@ -645,13 +558,8 @@ mod tests {
use crate::context::BinarySource; use crate::context::BinarySource;
fn load_fixture(name: &str) -> Vec<u8> { fn load_fixture(name: &str) -> Vec<u8> {
let path = format!( let path = format!("{}/tests/fixtures/{name}", env!("CARGO_MANIFEST_DIR"),);
"{}/tests/fixtures/{name}", std::fs::read(&path).unwrap_or_else(|e| panic!("fixture {path}: {e}"))
env!("CARGO_MANIFEST_DIR"),
);
std::fs::read(&path).unwrap_or_else(|e| {
panic!("fixture {path}: {e}")
})
} }
fn make_ctx(data: Vec<u8>) -> AnalysisContext { fn make_ctx(data: Vec<u8>) -> AnalysisContext {
@ -666,23 +574,12 @@ mod tests {
#[test] #[test]
fn ascii_extraction_min_length() { fn ascii_extraction_min_length() {
let data = let data = b"abc\x00abcdef\x00ab\x00longstring\x00";
b"abc\x00abcdef\x00ab\x00longstring\x00";
let result = extract_strings(data, None); let result = extract_strings(data, None);
let values: Vec<&str> = result let values: Vec<&str> = result.strings.iter().map(|s| s.value.as_str()).collect();
.strings assert!(!values.contains(&"abc"), "3-char string should be excluded");
.iter() assert!(!values.contains(&"ab"), "2-char string should be excluded");
.map(|s| s.value.as_str())
.collect();
assert!(
!values.contains(&"abc"),
"3-char string should be excluded"
);
assert!(
!values.contains(&"ab"),
"2-char string should be excluded"
);
assert!( assert!(
values.contains(&"abcdef"), values.contains(&"abcdef"),
"6-char string should be included" "6-char string should be included"
@ -704,57 +601,28 @@ mod tests {
data.push(0x00); data.push(0x00);
let result = extract_strings(&data, None); let result = extract_strings(&data, None);
let utf16_strings: Vec<&ExtractedString> = let utf16_strings: Vec<&ExtractedString> = result
result .strings
.strings
.iter()
.filter(|s| {
s.encoding == StringEncoding::Utf16Le
})
.collect();
assert!(
!utf16_strings.is_empty(),
"should extract UTF-16LE string"
);
assert!(utf16_strings
.iter() .iter()
.any(|s| s.value.contains("Hello"))); .filter(|s| s.encoding == StringEncoding::Utf16Le)
.collect();
assert!(!utf16_strings.is_empty(), "should extract UTF-16LE string");
assert!(utf16_strings.iter().any(|s| s.value.contains("Hello")));
} }
#[test] #[test]
fn category_url() { fn category_url() {
assert_eq!( assert_eq!(classify("https://evil.com/payload"), StringCategory::Url,);
classify("https://evil.com/payload"), assert_eq!(classify("http://malware.ru/dropper"), StringCategory::Url,);
StringCategory::Url, assert_eq!(classify("ftp://files.example.com"), StringCategory::Url,);
);
assert_eq!(
classify("http://malware.ru/dropper"),
StringCategory::Url,
);
assert_eq!(
classify("ftp://files.example.com"),
StringCategory::Url,
);
} }
#[test] #[test]
fn category_ip_address() { fn category_ip_address() {
assert_eq!( assert_eq!(classify("192.168.1.1"), StringCategory::IpAddress,);
classify("192.168.1.1"), assert_eq!(classify("10.0.0.1"), StringCategory::IpAddress,);
StringCategory::IpAddress, assert_ne!(classify("999.999.999.999"), StringCategory::IpAddress,);
); assert_ne!(classify("1.2.3"), StringCategory::IpAddress,);
assert_eq!(
classify("10.0.0.1"),
StringCategory::IpAddress,
);
assert_ne!(
classify("999.999.999.999"),
StringCategory::IpAddress,
);
assert_ne!(
classify("1.2.3"),
StringCategory::IpAddress,
);
} }
#[test] #[test]
@ -763,10 +631,7 @@ mod tests {
classify("HKLM\\Software\\Microsoft"), classify("HKLM\\Software\\Microsoft"),
StringCategory::RegistryKey, StringCategory::RegistryKey,
); );
assert_eq!( assert_eq!(classify("HKEY_LOCAL_MACHINE"), StringCategory::RegistryKey,);
classify("HKEY_LOCAL_MACHINE"),
StringCategory::RegistryKey,
);
} }
#[test] #[test]
@ -775,22 +640,13 @@ mod tests {
classify("C:\\Windows\\System32\\notepad.exe"), classify("C:\\Windows\\System32\\notepad.exe"),
StringCategory::FilePath, StringCategory::FilePath,
); );
assert_eq!( assert_eq!(classify("/tmp/output.log"), StringCategory::FilePath,);
classify("/tmp/output.log"), assert_eq!(classify("\\\\server\\share"), StringCategory::FilePath,);
StringCategory::FilePath,
);
assert_eq!(
classify("\\\\server\\share"),
StringCategory::FilePath,
);
} }
#[test] #[test]
fn category_shell_command() { fn category_shell_command() {
assert_eq!( assert_eq!(classify("cmd.exe /c whoami"), StringCategory::ShellCommand,);
classify("cmd.exe /c whoami"),
StringCategory::ShellCommand,
);
assert_eq!( assert_eq!(
classify("/bin/bash -c echo hi"), classify("/bin/bash -c echo hi"),
StringCategory::ShellCommand, StringCategory::ShellCommand,
@ -799,10 +655,7 @@ mod tests {
#[test] #[test]
fn category_packer_signature() { fn category_packer_signature() {
assert_eq!( assert_eq!(classify("UPX!"), StringCategory::PackerSignature,);
classify("UPX!"),
StringCategory::PackerSignature,
);
assert_eq!( assert_eq!(
classify("This is .themida packed"), classify("This is .themida packed"),
StringCategory::PackerSignature, StringCategory::PackerSignature,
@ -815,10 +668,7 @@ mod tests {
classify("VMware Virtual Platform"), classify("VMware Virtual Platform"),
StringCategory::AntiAnalysis, StringCategory::AntiAnalysis,
); );
assert_eq!( assert_eq!(classify("wireshark.exe"), StringCategory::AntiAnalysis,);
classify("wireshark.exe"),
StringCategory::AntiAnalysis,
);
} }
#[test] #[test]
@ -830,63 +680,34 @@ mod tests {
), ),
StringCategory::PersistencePath, StringCategory::PersistencePath,
); );
assert_eq!( assert_eq!(classify("/etc/crontab"), StringCategory::PersistencePath,);
classify("/etc/crontab"),
StringCategory::PersistencePath,
);
} }
#[test] #[test]
fn category_encoded_data() { fn category_encoded_data() {
assert_eq!( assert_eq!(
classify( classify("SGVsbG8gV29ybGQhIFRoaXMgaXM="),
"SGVsbG8gV29ybGQhIFRoaXMgaXM="
),
StringCategory::EncodedData,
);
assert_ne!(
classify("short"),
StringCategory::EncodedData, StringCategory::EncodedData,
); );
assert_ne!(classify("short"), StringCategory::EncodedData,);
} }
#[test] #[test]
fn category_email() { fn category_email() {
assert_eq!( assert_eq!(classify("user@example.com"), StringCategory::Email,);
classify("user@example.com"),
StringCategory::Email,
);
} }
#[test] #[test]
fn suspicious_flag_by_category() { fn suspicious_flag_by_category() {
assert!(is_category_suspicious( assert!(is_category_suspicious(&StringCategory::ShellCommand));
&StringCategory::ShellCommand assert!(is_category_suspicious(&StringCategory::AntiAnalysis));
)); assert!(is_category_suspicious(&StringCategory::PackerSignature));
assert!(is_category_suspicious( assert!(is_category_suspicious(&StringCategory::PersistencePath));
&StringCategory::AntiAnalysis assert!(is_category_suspicious(&StringCategory::EncodedData));
)); assert!(is_category_suspicious(&StringCategory::CryptoWallet));
assert!(is_category_suspicious( assert!(is_category_suspicious(&StringCategory::SuspiciousApi));
&StringCategory::PackerSignature assert!(!is_category_suspicious(&StringCategory::Generic));
)); assert!(!is_category_suspicious(&StringCategory::Url));
assert!(is_category_suspicious(
&StringCategory::PersistencePath
));
assert!(is_category_suspicious(
&StringCategory::EncodedData
));
assert!(is_category_suspicious(
&StringCategory::CryptoWallet
));
assert!(is_category_suspicious(
&StringCategory::SuspiciousApi
));
assert!(!is_category_suspicious(
&StringCategory::Generic
));
assert!(!is_category_suspicious(
&StringCategory::Url
));
} }
#[test] #[test]
@ -919,12 +740,11 @@ mod tests {
virtual_size: 100, virtual_size: 100,
raw_offset: 10, raw_offset: 10,
raw_size: 100, raw_size: 100,
permissions: permissions: crate::types::SectionPermissions {
crate::types::SectionPermissions { read: true,
read: true, write: false,
write: false, execute: false,
execute: false, },
},
sha256: String::new(), sha256: String::new(),
}]; }];

View File

@ -271,58 +271,38 @@ pub struct YaraScanner {
impl YaraScanner { impl YaraScanner {
pub fn new() -> Result<Self, EngineError> { pub fn new() -> Result<Self, EngineError> {
let mut compiler = yara_x::Compiler::new(); let mut compiler = yara_x::Compiler::new();
compiler.add_source(BUILTIN_RULES).map_err( compiler
|e| EngineError::Yara(e.to_string()), .add_source(BUILTIN_RULES)
)?; .map_err(|e| EngineError::Yara(e.to_string()))?;
Ok(Self { Ok(Self {
rules: compiler.build(), rules: compiler.build(),
}) })
} }
pub fn with_custom_rules( pub fn with_custom_rules(rules_dir: &Path) -> Result<Self, EngineError> {
rules_dir: &Path,
) -> Result<Self, EngineError> {
let mut compiler = yara_x::Compiler::new(); let mut compiler = yara_x::Compiler::new();
compiler.add_source(BUILTIN_RULES).map_err( compiler
|e| EngineError::Yara(e.to_string()), .add_source(BUILTIN_RULES)
)?; .map_err(|e| EngineError::Yara(e.to_string()))?;
if rules_dir.is_dir() { if rules_dir.is_dir() {
for entry in std::fs::read_dir(rules_dir) for entry in std::fs::read_dir(rules_dir)
.map_err(|e| { .map_err(|e| EngineError::Yara(format!("failed to read rules dir: {e}")))?
EngineError::Yara(format!(
"failed to read rules dir: {e}"
))
})?
{ {
let entry = entry.map_err(|e| { let entry =
EngineError::Yara(format!( entry.map_err(|e| EngineError::Yara(format!("dir entry error: {e}")))?;
"dir entry error: {e}"
))
})?;
let path = entry.path(); let path = entry.path();
if path if path
.extension() .extension()
.is_some_and(|ext| ext == "yar" || ext == "yara") .is_some_and(|ext| ext == "yar" || ext == "yara")
{ {
let source = std::fs::read_to_string( let source = std::fs::read_to_string(&path).map_err(|e| {
&path, EngineError::Yara(format!("failed to read {}: {e}", path.display()))
) })?;
.map_err(|e| { compiler.add_source(source.as_str()).map_err(|e| {
EngineError::Yara(format!( EngineError::Yara(format!("compile error in {}: {e}", path.display()))
"failed to read {}: {e}",
path.display()
))
})?; })?;
compiler
.add_source(source.as_str())
.map_err(|e| {
EngineError::Yara(format!(
"compile error in {}: {e}",
path.display()
))
})?;
} }
} }
} }
@ -332,22 +312,15 @@ impl YaraScanner {
}) })
} }
pub fn scan( pub fn scan(&self, data: &[u8]) -> Result<Vec<YaraMatch>, EngineError> {
&self, let mut scanner = yara_x::Scanner::new(&self.rules);
data: &[u8], let results = scanner
) -> Result<Vec<YaraMatch>, EngineError> { .scan(data)
let mut scanner = .map_err(|e| EngineError::Yara(e.to_string()))?;
yara_x::Scanner::new(&self.rules);
let results = scanner.scan(data).map_err(
|e| EngineError::Yara(e.to_string()),
)?;
let mut matches = Vec::new(); let mut matches = Vec::new();
for rule in results.matching_rules() { for rule in results.matching_rules() {
let tags: Vec<String> = rule let tags: Vec<String> = rule.tags().map(|t| t.identifier().to_string()).collect();
.tags()
.map(|t| t.identifier().to_string())
.collect();
let mut description = None; let mut description = None;
let mut category = None; let mut category = None;
@ -356,20 +329,17 @@ impl YaraScanner {
match key { match key {
"description" => { "description" => {
if let yara_x::MetaValue::String(s) = value { if let yara_x::MetaValue::String(s) = value {
description = description = Some(s.to_string());
Some(s.to_string());
} }
} }
"category" => { "category" => {
if let yara_x::MetaValue::String(s) = value { if let yara_x::MetaValue::String(s) = value {
category = category = Some(s.to_string());
Some(s.to_string());
} }
} }
"severity" => { "severity" => {
if let yara_x::MetaValue::String(s) = value { if let yara_x::MetaValue::String(s) = value {
severity = severity = Some(s.to_string());
Some(s.to_string());
} }
} }
_ => {} _ => {}
@ -378,25 +348,18 @@ impl YaraScanner {
let mut matched_strings = Vec::new(); let mut matched_strings = Vec::new();
for pattern in rule.patterns() { for pattern in rule.patterns() {
let id = pattern let id = pattern.identifier().to_string();
.identifier() let count = pattern.matches().count();
.to_string();
let count =
pattern.matches().count();
if count > 0 { if count > 0 {
matched_strings.push( matched_strings.push(YaraStringMatch {
YaraStringMatch { identifier: id,
identifier: id, match_count: count,
match_count: count, });
},
);
} }
} }
matches.push(YaraMatch { matches.push(YaraMatch {
rule_name: rule rule_name: rule.identifier().to_string(),
.identifier()
.to_string(),
tags, tags,
metadata: YaraMetadata { metadata: YaraMetadata {
description, description,
@ -435,42 +398,25 @@ mod tests {
let upx_match = result let upx_match = result
.iter() .iter()
.find(|m| m.rule_name == "suspicious_upx_packed"); .find(|m| m.rule_name == "suspicious_upx_packed");
assert!( assert!(upx_match.is_some(), "should detect UPX packer signature");
upx_match.is_some(), let meta = &upx_match.unwrap().metadata;
"should detect UPX packer signature" assert_eq!(meta.category.as_deref(), Some("packer"));
);
let meta =
&upx_match.unwrap().metadata;
assert_eq!(
meta.category.as_deref(),
Some("packer")
);
} }
#[test] #[test]
fn detects_process_injection() { fn detects_process_injection() {
let mut data = Vec::new(); let mut data = Vec::new();
data.extend_from_slice( data.extend_from_slice(b"\x00\x00VirtualAllocEx\x00\x00");
b"\x00\x00VirtualAllocEx\x00\x00", data.extend_from_slice(b"\x00\x00WriteProcessMemory\x00\x00");
); data.extend_from_slice(b"\x00\x00CreateRemoteThread\x00\x00");
data.extend_from_slice(
b"\x00\x00WriteProcessMemory\x00\x00",
);
data.extend_from_slice(
b"\x00\x00CreateRemoteThread\x00\x00",
);
data.extend_from_slice(&[0u8; 256]); data.extend_from_slice(&[0u8; 256]);
let scanner = YaraScanner::new().unwrap(); let scanner = YaraScanner::new().unwrap();
let result = scanner.scan(&data).unwrap(); let result = scanner.scan(&data).unwrap();
let injection = result.iter().find(|m| { let injection = result
m.rule_name .iter()
== "suspicious_process_injection" .find(|m| m.rule_name == "suspicious_process_injection");
}); assert!(injection.is_some(), "should detect process injection APIs");
assert!(
injection.is_some(),
"should detect process injection APIs"
);
} }
#[test] #[test]
@ -480,9 +426,7 @@ mod tests {
let result = scanner.scan(data).unwrap(); let result = scanner.scan(data).unwrap();
let suspicious: Vec<_> = result let suspicious: Vec<_> = result
.iter() .iter()
.filter(|m| { .filter(|m| m.rule_name != "suspicious_obfuscation")
m.rule_name != "suspicious_obfuscation"
})
.collect(); .collect();
assert!( assert!(
suspicious.is_empty(), suspicious.is_empty(),
@ -492,13 +436,8 @@ mod tests {
} }
fn load_fixture(name: &str) -> Vec<u8> { fn load_fixture(name: &str) -> Vec<u8> {
let path = format!( let path = format!("{}/tests/fixtures/{name}", env!("CARGO_MANIFEST_DIR"),);
"{}/tests/fixtures/{name}", std::fs::read(&path).unwrap_or_else(|e| panic!("fixture {path}: {e}"))
env!("CARGO_MANIFEST_DIR"),
);
std::fs::read(&path).unwrap_or_else(|e| {
panic!("fixture {path}: {e}")
})
} }
#[test] #[test]

View File

@ -20,24 +20,19 @@
// lib.rs - AnalysisEngine // lib.rs - AnalysisEngine
// types.rs - BinaryFormat // types.rs - BinaryFormat
use axumortem_engine::types::BinaryFormat;
use axumortem_engine::AnalysisEngine; use axumortem_engine::AnalysisEngine;
use axumortem_engine::types::BinaryFormat;
fn load_fixture(name: &str) -> Vec<u8> { fn load_fixture(name: &str) -> Vec<u8> {
let path = format!( let path = format!("{}/tests/fixtures/{name}", env!("CARGO_MANIFEST_DIR"),);
"{}/tests/fixtures/{name}", std::fs::read(&path).unwrap_or_else(|e| panic!("fixture {path}: {e}"))
env!("CARGO_MANIFEST_DIR"),
);
std::fs::read(&path)
.unwrap_or_else(|e| panic!("fixture {path}: {e}"))
} }
#[test] #[test]
fn full_pipeline_elf() { fn full_pipeline_elf() {
let engine = AnalysisEngine::new().unwrap(); let engine = AnalysisEngine::new().unwrap();
let data = load_fixture("hello_elf"); let data = load_fixture("hello_elf");
let (ctx, report) = let (ctx, report) = engine.analyze(&data, "hello_elf");
engine.analyze(&data, "hello_elf");
assert!( assert!(
report.all_succeeded(), report.all_succeeded(),
@ -58,8 +53,7 @@ fn full_pipeline_elf() {
assert!(ctx.entropy_result.is_some()); assert!(ctx.entropy_result.is_some());
assert!(ctx.disassembly_result.is_some()); assert!(ctx.disassembly_result.is_some());
let disasm = let disasm = ctx.disassembly_result.as_ref().unwrap();
ctx.disassembly_result.as_ref().unwrap();
assert!(disasm.total_functions > 0); assert!(disasm.total_functions > 0);
assert!(disasm.total_instructions > 0); assert!(disasm.total_instructions > 0);
@ -73,8 +67,7 @@ fn full_pipeline_elf() {
fn full_pipeline_stripped_elf() { fn full_pipeline_stripped_elf() {
let engine = AnalysisEngine::new().unwrap(); let engine = AnalysisEngine::new().unwrap();
let data = load_fixture("hello_elf_stripped"); let data = load_fixture("hello_elf_stripped");
let (ctx, report) = let (ctx, report) = engine.analyze(&data, "hello_elf_stripped");
engine.analyze(&data, "hello_elf_stripped");
assert!(report.all_succeeded()); assert!(report.all_succeeded());
@ -98,15 +91,11 @@ fn sha256_computed_correctly() {
fn invalid_binary_handled() { fn invalid_binary_handled() {
let engine = AnalysisEngine::new().unwrap(); let engine = AnalysisEngine::new().unwrap();
let data = vec![0xDE, 0xAD, 0xBE, 0xEF]; let data = vec![0xDE, 0xAD, 0xBE, 0xEF];
let (_, report) = let (_, report) = engine.analyze(&data, "garbage.bin");
engine.analyze(&data, "garbage.bin");
assert!( assert!(
!report.all_succeeded(), !report.all_succeeded(),
"invalid binary should cause format pass failure" "invalid binary should cause format pass failure"
); );
assert!(report assert!(report.failed_passes().iter().any(|p| p.name == "format"));
.failed_passes()
.iter()
.any(|p| p.name == "format"));
} }

View File

@ -17,8 +17,6 @@ pub mod queries;
use sqlx::PgPool; use sqlx::PgPool;
pub async fn run_migrations( pub async fn run_migrations(pool: &PgPool) -> Result<(), sqlx::migrate::MigrateError> {
pool: &PgPool,
) -> Result<(), sqlx::migrate::MigrateError> {
sqlx::migrate!("./migrations").run(pool).await sqlx::migrate!("./migrations").run(pool).await
} }

View File

@ -38,6 +38,7 @@ pub struct AnalysisRow {
} }
#[derive(FromRow)] #[derive(FromRow)]
#[allow(dead_code)]
pub struct PassResultRow { pub struct PassResultRow {
pub id: Uuid, pub id: Uuid,
pub analysis_id: Uuid, pub analysis_id: Uuid,

View File

@ -22,33 +22,23 @@
use sqlx::{PgPool, Postgres, Transaction}; use sqlx::{PgPool, Postgres, Transaction};
use uuid::Uuid; use uuid::Uuid;
use super::models::{ use super::models::{AnalysisRow, NewAnalysis, NewPassResult, PassResultRow};
AnalysisRow, NewAnalysis, NewPassResult,
PassResultRow,
};
pub async fn find_slug_by_sha256( pub async fn find_slug_by_sha256(
pool: &PgPool, pool: &PgPool,
sha256: &str, sha256: &str,
) -> Result<Option<String>, sqlx::Error> { ) -> Result<Option<String>, sqlx::Error> {
sqlx::query_scalar( sqlx::query_scalar("SELECT slug FROM analyses WHERE sha256 = $1")
"SELECT slug FROM analyses WHERE sha256 = $1", .bind(sha256)
) .fetch_optional(pool)
.bind(sha256) .await
.fetch_optional(pool)
.await
} }
pub async fn find_by_slug( pub async fn find_by_slug(pool: &PgPool, slug: &str) -> Result<Option<AnalysisRow>, sqlx::Error> {
pool: &PgPool, sqlx::query_as::<_, AnalysisRow>("SELECT * FROM analyses WHERE slug = $1")
slug: &str, .bind(slug)
) -> Result<Option<AnalysisRow>, sqlx::Error> { .fetch_optional(pool)
sqlx::query_as::<_, AnalysisRow>( .await
"SELECT * FROM analyses WHERE slug = $1",
)
.bind(slug)
.fetch_optional(pool)
.await
} }
pub async fn find_pass_results( pub async fn find_pass_results(

View File

@ -16,9 +16,9 @@
// routes/upload.rs - returned from upload handler // routes/upload.rs - returned from upload handler
// routes/analysis.rs - returned from analysis lookup // routes/analysis.rs - returned from analysis lookup
use axum::Json;
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::response::{IntoResponse, Response}; use axum::response::{IntoResponse, Response};
use axum::Json;
use serde::Serialize; use serde::Serialize;
#[derive(Serialize)] #[derive(Serialize)]
@ -32,6 +32,7 @@ struct ErrorDetail {
message: String, message: String,
} }
#[allow(dead_code)]
pub enum ApiError { pub enum ApiError {
NoFile, NoFile,
FileTooLarge { max_bytes: usize }, FileTooLarge { max_bytes: usize },
@ -71,37 +72,23 @@ impl IntoResponse for ApiError {
Self::NoFile => ( Self::NoFile => (
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
"NO_FILE", "NO_FILE",
"No file was provided in the upload" "No file was provided in the upload".to_string(),
.to_string(),
), ),
Self::FileTooLarge { max_bytes } => ( Self::FileTooLarge { max_bytes } => (
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
"FILE_TOO_LARGE", "FILE_TOO_LARGE",
format!( format!("File exceeds maximum allowed size of {} bytes", max_bytes),
"File exceeds maximum allowed size of {} bytes",
max_bytes
),
),
Self::InvalidBinary { reason } => (
StatusCode::BAD_REQUEST,
"INVALID_BINARY",
reason,
),
Self::AnalysisFailed { reason } => (
StatusCode::INTERNAL_SERVER_ERROR,
"ANALYSIS_FAILED",
reason,
), ),
Self::InvalidBinary { reason } => (StatusCode::BAD_REQUEST, "INVALID_BINARY", reason),
Self::AnalysisFailed { reason } => {
(StatusCode::INTERNAL_SERVER_ERROR, "ANALYSIS_FAILED", reason)
}
Self::NotFound { resource } => ( Self::NotFound { resource } => (
StatusCode::NOT_FOUND, StatusCode::NOT_FOUND,
"NOT_FOUND", "NOT_FOUND",
format!("{resource} not found"), format!("{resource} not found"),
), ),
Self::Internal { reason } => ( Self::Internal { reason } => (StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL", reason),
StatusCode::INTERNAL_SERVER_ERROR,
"INTERNAL",
reason,
),
}; };
( (

View File

@ -49,11 +49,7 @@ async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt() tracing_subscriber::fmt()
.with_env_filter( .with_env_filter(
EnvFilter::try_from_default_env() EnvFilter::try_from_default_env()
.unwrap_or_else(|_| { .unwrap_or_else(|_| EnvFilter::new("info,tower_http=debug")),
EnvFilter::new(
"info,tower_http=debug",
)
}),
) )
.init(); .init();
@ -67,10 +63,8 @@ async fn main() -> anyhow::Result<()> {
.await .await
.context("failed to run database migrations")?; .context("failed to run database migrations")?;
let engine = axumortem_engine::AnalysisEngine::new() let engine =
.context( axumortem_engine::AnalysisEngine::new().context("failed to initialize analysis engine")?;
"failed to initialize analysis engine",
)?;
let config = Arc::new(config); let config = Arc::new(config);
@ -83,18 +77,14 @@ async fn main() -> anyhow::Result<()> {
let layers = ServiceBuilder::new() let layers = ServiceBuilder::new()
.layer(TraceLayer::new_for_http()) .layer(TraceLayer::new_for_http())
.layer(middleware::cors::layer(&config)) .layer(middleware::cors::layer(&config))
.layer(DefaultBodyLimit::max( .layer(DefaultBodyLimit::max(config.max_upload_size));
config.max_upload_size,
));
let app = let app = routes::api_router().layer(layers).with_state(state);
routes::api_router().layer(layers).with_state(state);
let bind_address = config.bind_address(); let bind_address = config.bind_address();
let listener = let listener = tokio::net::TcpListener::bind(&bind_address)
tokio::net::TcpListener::bind(&bind_address) .await
.await .context("failed to bind TCP listener")?;
.context("failed to bind TCP listener")?;
tracing::info!("listening on {}", bind_address); tracing::info!("listening on {}", bind_address);

View File

@ -13,17 +13,15 @@
// config.rs - AppConfig.cors_origin // config.rs - AppConfig.cors_origin
// main.rs - applied as tower layer // main.rs - applied as tower layer
use axum::http::header::{HeaderName, ACCEPT, CONTENT_TYPE};
use axum::http::Method; use axum::http::Method;
use axum::http::header::{ACCEPT, CONTENT_TYPE, HeaderName};
use tower_http::cors::{Any, CorsLayer}; use tower_http::cors::{Any, CorsLayer};
use crate::config::AppConfig; use crate::config::AppConfig;
const ALLOWED_METHODS: [Method; 3] = const ALLOWED_METHODS: [Method; 3] = [Method::GET, Method::POST, Method::OPTIONS];
[Method::GET, Method::POST, Method::OPTIONS];
const ALLOWED_HEADERS: [HeaderName; 2] = const ALLOWED_HEADERS: [HeaderName; 2] = [CONTENT_TYPE, ACCEPT];
[CONTENT_TYPE, ACCEPT];
pub fn layer(config: &AppConfig) -> CorsLayer { pub fn layer(config: &AppConfig) -> CorsLayer {
let base = CorsLayer::new() let base = CorsLayer::new()

View File

@ -17,8 +17,8 @@
use std::collections::HashMap; use std::collections::HashMap;
use axum::extract::{Path, State};
use axum::Json; use axum::Json;
use axum::extract::{Path, State};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::Serialize; use serde::Serialize;
use uuid::Uuid; use uuid::Uuid;
@ -53,15 +53,12 @@ pub async fn get_by_slug(
resource: format!("analysis '{slug}'"), resource: format!("analysis '{slug}'"),
})?; })?;
let pass_rows = let pass_rows = queries::find_pass_results(&state.db, row.id).await?;
queries::find_pass_results(&state.db, row.id)
.await?;
let passes: HashMap<String, serde_json::Value> = let passes: HashMap<String, serde_json::Value> = pass_rows
pass_rows .into_iter()
.into_iter() .map(|p| (p.pass_name, p.result))
.map(|p| (p.pass_name, p.result)) .collect();
.collect();
Ok(Json(AnalysisResponse { Ok(Json(AnalysisResponse {
id: row.id, id: row.id,

View File

@ -11,8 +11,8 @@
// Connects to: // Connects to:
// state.rs - AppState.db // state.rs - AppState.db
use axum::extract::State;
use axum::Json; use axum::Json;
use axum::extract::State;
use serde::Serialize; use serde::Serialize;
use crate::state::AppState; use crate::state::AppState;
@ -23,15 +23,11 @@ pub(crate) struct HealthResponse {
database: &'static str, database: &'static str,
} }
pub async fn check( pub async fn check(State(state): State<AppState>) -> Json<HealthResponse> {
State(state): State<AppState>, let db_status = match sqlx::query("SELECT 1").execute(&state.db).await {
) -> Json<HealthResponse> { Ok(_) => "connected",
let db_status = Err(_) => "disconnected",
match sqlx::query("SELECT 1").execute(&state.db).await };
{
Ok(_) => "connected",
Err(_) => "disconnected",
};
Json(HealthResponse { Json(HealthResponse {
status: "ok", status: "ok",

View File

@ -18,8 +18,8 @@ mod analysis;
mod health; mod health;
mod upload; mod upload;
use axum::routing::{get, post};
use axum::Router; use axum::Router;
use axum::routing::{get, post};
use crate::state::AppState; use crate::state::AppState;
@ -27,8 +27,5 @@ pub fn api_router() -> Router<AppState> {
Router::new() Router::new()
.route("/api/health", get(health::check)) .route("/api/health", get(health::check))
.route("/api/upload", post(upload::handle)) .route("/api/upload", post(upload::handle))
.route( .route("/api/analysis/{slug}", get(analysis::get_by_slug))
"/api/analysis/{slug}",
get(analysis::get_by_slug),
)
} }

View File

@ -27,8 +27,8 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use axum::extract::{Multipart, State};
use axum::Json; use axum::Json;
use axum::extract::{Multipart, State};
use serde::Serialize; use serde::Serialize;
use uuid::Uuid; use uuid::Uuid;
@ -42,9 +42,7 @@ use crate::state::AppState;
const SLUG_LENGTH: usize = 12; const SLUG_LENGTH: usize = 12;
const PASS_NAME_MAP: &[(&str, &str)] = &[ const PASS_NAME_MAP: &[(&str, &str)] = &[("disasm", "disassembly")];
("disasm", "disassembly"),
];
#[derive(Serialize)] #[derive(Serialize)]
pub(crate) struct UploadResponse { pub(crate) struct UploadResponse {
@ -56,32 +54,19 @@ pub async fn handle(
State(state): State<AppState>, State(state): State<AppState>,
mut multipart: Multipart, mut multipart: Multipart,
) -> Result<Json<UploadResponse>, ApiError> { ) -> Result<Json<UploadResponse>, ApiError> {
let (file_name, data) = let (file_name, data) = extract_file(&mut multipart).await?;
extract_file(&mut multipart).await?;
let sha256 = let sha256 = axumortem_engine::sha256_hex(&data);
axumortem_engine::sha256_hex(&data);
if let Some(slug) = if let Some(slug) = queries::find_slug_by_sha256(&state.db, &sha256).await? {
queries::find_slug_by_sha256( return Ok(Json(UploadResponse { slug, cached: true }));
&state.db, &sha256,
)
.await?
{
return Ok(Json(UploadResponse {
slug,
cached: true,
}));
} }
let engine = Arc::clone(&state.engine); let engine = Arc::clone(&state.engine);
let name_clone = file_name.clone(); let name_clone = file_name.clone();
let (ctx, report) = let (ctx, report) =
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || engine.analyze(&data, &name_clone)).await?;
engine.analyze(&data, &name_clone)
})
.await?;
let fmt = ctx.format_result.as_ref(); let fmt = ctx.format_result.as_ref();
let threat = ctx.threat_result.as_ref(); let threat = ctx.threat_result.as_ref();
@ -91,29 +76,19 @@ pub async fn handle(
sha256, sha256,
file_name, file_name,
file_size: ctx.file_size as i64, file_size: ctx.file_size as i64,
format: fmt format: fmt.map(|f| f.format.to_string()).unwrap_or_default(),
.map(|f| f.format.to_string()) architecture: fmt.map(|f| f.architecture.to_string()).unwrap_or_default(),
.unwrap_or_default(), entry_point: fmt.map(|f| f.entry_point as i64),
architecture: fmt threat_score: threat.map(|t| t.total_score as i32),
.map(|f| f.architecture.to_string()) risk_level: threat.map(|t| t.risk_level.to_string()),
.unwrap_or_default(),
entry_point: fmt
.map(|f| f.entry_point as i64),
threat_score: threat
.map(|t| t.total_score as i32),
risk_level: threat
.map(|t| t.risk_level.to_string()),
slug: slug.clone(), slug: slug.clone(),
}; };
let mut tx = state.db.begin().await?; let mut tx = state.db.begin().await?;
let row = let row = queries::insert_analysis(&mut tx, &new_analysis).await?;
queries::insert_analysis(&mut tx, &new_analysis)
.await?;
let pass_results = let pass_results = build_pass_results(&ctx, &report, row.id)?;
build_pass_results(&ctx, &report, row.id)?;
for pr in &pass_results { for pr in &pass_results {
queries::insert_pass_result(&mut tx, pr).await?; queries::insert_pass_result(&mut tx, pr).await?;
} }
@ -126,9 +101,7 @@ pub async fn handle(
})) }))
} }
async fn extract_file( async fn extract_file(multipart: &mut Multipart) -> Result<(String, Vec<u8>), ApiError> {
multipart: &mut Multipart,
) -> Result<(String, Vec<u8>), ApiError> {
while let Some(field) = multipart while let Some(field) = multipart
.next_field() .next_field()
.await .await
@ -137,16 +110,10 @@ async fn extract_file(
})? })?
{ {
if field.name() == Some("file") { if field.name() == Some("file") {
let name = field let name = field.file_name().unwrap_or("unknown").to_string();
.file_name() let data = field.bytes().await.map_err(|e| ApiError::Internal {
.unwrap_or("unknown") reason: e.to_string(),
.to_string(); })?;
let data = field
.bytes()
.await
.map_err(|e| ApiError::Internal {
reason: e.to_string(),
})?;
return Ok((name, data.to_vec())); return Ok((name, data.to_vec()));
} }
} }
@ -181,12 +148,9 @@ fn build_pass_results(
if let Some(ref r) = ctx.$field { if let Some(ref r) = ctx.$field {
results.push(NewPassResult { results.push(NewPassResult {
analysis_id, analysis_id,
pass_name: api_name($name) pass_name: api_name($name).to_string(),
.to_string(),
result: serde_json::to_value(r)?, result: serde_json::to_value(r)?,
duration_ms: durations duration_ms: durations.get($name).map(|&d| d as i32),
.get($name)
.map(|&d| d as i32),
}); });
} }
}; };

View File

@ -24,5 +24,6 @@ use crate::config::AppConfig;
pub struct AppState { pub struct AppState {
pub db: PgPool, pub db: PgPool,
pub engine: Arc<AnalysisEngine>, pub engine: Arc<AnalysisEngine>,
#[allow(dead_code)]
pub config: Arc<AppConfig>, pub config: Arc<AppConfig>,
} }

View File

@ -109,6 +109,7 @@ export function Component(): React.ReactElement {
width="100%" width="100%"
height="100%" height="100%"
preserveAspectRatio="none" preserveAspectRatio="none"
aria-hidden="true"
> >
<filter id="grain-bat"> <filter id="grain-bat">
<feTurbulence <feTurbulence
@ -248,7 +249,9 @@ export function Component(): React.ReactElement {
<footer className={styles.footer}> <footer className={styles.footer}>
<span>&copy; ANGELAMOS 2026</span> <span>&copy; ANGELAMOS 2026</span>
<span className={styles.footerDesignation}>SYS AXM-BDE // UNIT-001</span> <span className={styles.footerDesignation}>
SYS AXM-BDE {'//'} UNIT-001
</span>
<span>AXUMORTEM</span> <span>AXUMORTEM</span>
</footer> </footer>
</div> </div>

View File

@ -0,0 +1,16 @@
# ©AngelaMos | 2026
# .ameba.yml
#
# Disabled rules document intentional patterns in this codebase
# - Naming/BlockParameterName: idiomatic Crystal uses short block params like |c|, |b|
# - Lint/NotNil: .not_nil! used as explicit runtime contract assertion after upstream
# validation; raising NilAssertionError is the desired crash mode for misconfiguration
# Raised thresholds match the natural shape of dispatch/subscriber handlers
Naming/BlockParameterName:
Enabled: false
Lint/NotNil:
Enabled: false
Metrics/CyclomaticComplexity:
MaxComplexity: 20

View File

@ -1,5 +1,9 @@
version: 2.0 version: 2.0
shards: shards:
ameba:
git: https://github.com/crystal-ameba/ameba.git
version: 1.6.4
db: db:
git: https://github.com/crystal-lang/crystal-db.git git: https://github.com/crystal-lang/crystal-db.git
version: 0.11.0 version: 0.11.0

View File

@ -33,3 +33,6 @@ development_dependencies:
webmock: webmock:
github: manastech/webmock.cr github: manastech/webmock.cr
version: ~> 0.9 version: ~> 0.9
ameba:
github: crystal-ameba/ameba
version: ~> 1.6.4

View File

@ -79,7 +79,7 @@ module CRE::Aws
end end
private def canonical_query(query : String?) : String private def canonical_query(query : String?) : String
return "" unless query && !query.empty? return "" if query.nil? || query.empty?
params = [] of {String, String} params = [] of {String, String}
query.split('&') do |pair| query.split('&') do |pair|
eq = pair.index('=') eq = pair.index('=')
@ -99,7 +99,7 @@ module CRE::Aws
private def canonical_headers_and_list(headers : HTTP::Headers) : {String, String} private def canonical_headers_and_list(headers : HTTP::Headers) : {String, String}
sorted = headers.to_a.map { |name, values| sorted = headers.to_a.map { |name, values|
{name.downcase, values.first.strip.gsub(/\s+/, " ")} {name.downcase, values.first.strip.gsub(/\s+/, " ")}
}.sort_by { |entry| entry[0] } }.sort_by! { |entry| entry[0] }
canonical = sorted.map { |k, v| "#{k}:#{v}\n" }.join canonical = sorted.map { |k, v| "#{k}:#{v}\n" }.join
list = sorted.map(&.[0]).join(';') list = sorted.map(&.[0]).join(';')

View File

@ -32,7 +32,7 @@ module CRE::Cli::Commands
CRE::Policy::Evaluator.new(bus, persist).evaluate_all CRE::Policy::Evaluator.new(bus, persist).evaluate_all
sleep 0.1.seconds sleep 0.1.seconds
violations = drain(ch).select(&.is_a?(CRE::Events::PolicyViolation)).map(&.as(CRE::Events::PolicyViolation)) violations = drain(ch).select(CRE::Events::PolicyViolation).map(&.as(CRE::Events::PolicyViolation))
bus.stop bus.stop
persist.close persist.close

View File

@ -19,7 +19,7 @@ module CRE::Demo
bus.run bus.run
tui.start tui.start
script_fiber = spawn do spawn do
run_script(bus, seconds) run_script(bus, seconds)
end end

View File

@ -59,7 +59,7 @@ module CRE::Engine
@persistence.rotations.update_state(rotation_id, Persistence::RotationState::Applying) @persistence.rotations.update_state(rotation_id, Persistence::RotationState::Applying)
@bus.publish Events::RotationStepCompleted.new(c.id, rotation_id, :generate) @bus.publish Events::RotationStepCompleted.new(c.id, rotation_id, :generate)
current_step = :apply current_step = :apply # ameba:disable Lint/UselessAssign — read in rescue
@bus.publish Events::RotationStepStarted.new(c.id, rotation_id, :apply) @bus.publish Events::RotationStepStarted.new(c.id, rotation_id, :apply)
rotator.apply(c, new_secret) rotator.apply(c, new_secret)
@persistence.rotations.update_state(rotation_id, Persistence::RotationState::Verifying) @persistence.rotations.update_state(rotation_id, Persistence::RotationState::Verifying)
@ -83,8 +83,8 @@ module CRE::Engine
if (ns = new_secret) && (current_step == :apply || current_step == :verify) if (ns = new_secret) && (current_step == :apply || current_step == :verify)
begin begin
rotator.rollback_apply(c, ns) rotator.rollback_apply(c, ns)
rescue rb rescue error
Log.error(exception: rb) { "rollback_apply failed for credential #{c.id}" } Log.error(exception: error) { "rollback_apply failed for credential #{c.id}" }
end end
end end
finalize_failure(c, rotation_id, current_step, ex) finalize_failure(c, rotation_id, current_step, ex)

View File

@ -40,11 +40,11 @@ module CRE::Persistence::Postgres
@audit ||= AuditRepo.new(@db) @audit ||= AuditRepo.new(@db)
end end
def transaction(&block : ->) : Nil def transaction(& : ->) : Nil
@db.transaction { yield } @db.transaction { yield }
end end
def with_advisory_lock(key : Int64, &block : ->) : Nil def with_advisory_lock(key : Int64, & : ->) : Nil
@db.transaction do |tx| @db.transaction do |tx|
tx.connection.exec("SELECT pg_advisory_xact_lock($1)", key) tx.connection.exec("SELECT pg_advisory_xact_lock($1)", key)
yield yield

View File

@ -48,11 +48,11 @@ module CRE::Persistence::Sqlite
@audit ||= AuditRepo.new(@db) @audit ||= AuditRepo.new(@db)
end end
def transaction(&block : ->) : Nil def transaction(& : ->) : Nil
@db.transaction { yield } @db.transaction { yield }
end end
def with_advisory_lock(key : Int64, &block : ->) : Nil def with_advisory_lock(key : Int64, & : ->) : Nil
_ = key # SQLite is single-process; the mutex is sufficient _ = key # SQLite is single-process; the mutex is sufficient
@mutex.synchronize { yield } @mutex.synchronize { yield }
end end

View File

@ -25,7 +25,7 @@ module CRE::Policy::Dsl
# `enforce :rotate_immediatly` fail at compile time. Splat-symbol args # `enforce :rotate_immediatly` fail at compile time. Splat-symbol args
# (`notify_via :telegram, :structured_log`) are validated at policy # (`notify_via :telegram, :structured_log`) are validated at policy
# registration time and raise `BuilderError` on typos. # registration time and raise `BuilderError` on typos.
def policy(name : String, &block) def policy(name : String, &)
builder = CRE::Policy::Builder.new(name) builder = CRE::Policy::Builder.new(name)
with builder yield with builder yield
CRE::Policy::REGISTRY << builder.build CRE::Policy::REGISTRY << builder.build

View File

@ -54,7 +54,7 @@ module CRE::Policy
return if @policies.empty? return if @policies.empty?
seen = Set(UUID).new seen = Set(UUID).new
@policies.group_by(&.max_age).each do |max_age, policies_with_age| @policies.group_by(&.max_age).each do |max_age, _|
@persistence.credentials.overdue(now, max_age).each do |c| @persistence.credentials.overdue(now, max_age).each do |c|
next if seen.includes?(c.id) next if seen.includes?(c.id)
seen << c.id seen << c.id

View File

@ -50,7 +50,7 @@ module CRE::Rotators
pending = pending_path(path) pending = pending_path(path)
existing = File.exists?(path) ? File.read(path) : "" existing = File.exists?(path) ? File.read(path) : ""
lines = existing.lines(chomp: true).reject { |line| line.strip.starts_with?("#{key}=") } lines = existing.lines(chomp: true).reject(&.strip.starts_with?("#{key}="))
new_value = String.new(s.ciphertext) new_value = String.new(s.ciphertext)
lines << "#{key}=#{new_value}" lines << "#{key}=#{new_value}"

View File

@ -1,31 +1,39 @@
# ©AngelaMos | 2026 # ©AngelaMos | 2026
# .golangci.yml # .golangci.yml
version: "2"
run: run:
timeout: 5m timeout: 5m
linters: linters:
default: none
enable: enable:
- errcheck - errcheck
- govet - govet
- staticcheck - staticcheck
- unused - unused
- gosimple
- ineffassign - ineffassign
- typecheck
- gofmt
- gocritic - gocritic
- gosec - gosec
- misspell - misspell
- unconvert - unconvert
- unparam - unparam
- prealloc - prealloc
settings:
linters-settings: errcheck:
gocritic: exclude-functions:
enabled-tags: - fmt.Fprint
- diagnostic - fmt.Fprintf
- performance - fmt.Fprintln
gosec: - (io.Closer).Close
excludes: gocritic:
- G304 enabled-tags:
- diagnostic
- performance
disabled-checks:
- hugeParam
- rangeValCopy
gosec:
excludes:
- G304
- G704

Some files were not shown because too many files have changed in this diff Show More